From af6bea5acc133a11825ce5a82d94f168abc825b1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 6 Jul 2020 11:21:03 +1200 Subject: [PATCH] feat(kubernetes): introduce kubernetes support (#3987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: xAt0mZ * 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 * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: xAt0mZ * 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 * 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 * 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 * feat(kubernetes): Introduce a "used by" column in the volume list view (#303) Co-authored-by: xAt0mZ Co-authored-by: Maxime Bajeux Co-authored-by: xAt0mZ --- .codeclimate.yml | 58 +- .github/ISSUE_TEMPLATE/Bug_report.md | 95 +- CONTRIBUTING.md | 26 +- api/chisel/service.go | 10 +- api/cmd/portainer/main.go | 121 +- api/docker/client.go | 4 +- api/docker/snapshot.go | 43 +- api/docker/snapshotter.go | 28 - api/exec/kubernetes_deploy.go | 89 + api/exec/swarm_stack.go | 2 +- api/go.mod | 4 + api/go.sum | 151 ++ api/http/handler/auth/handler.go | 16 +- api/http/handler/auth/logout.go | 21 + .../edgegroups/associated_endpoints.go | 2 +- .../handler/edgegroups/edgegroup_create.go | 2 +- .../handler/edgegroups/edgegroup_update.go | 2 +- api/http/handler/edgejobs/edgejob_create.go | 2 +- api/http/handler/edgejobs/edgejob_update.go | 2 +- api/http/handler/endpointgroups/endpoints.go | 2 +- api/http/handler/endpointproxy/handler.go | 2 + .../handler/endpointproxy/proxy_docker.go | 2 +- .../handler/endpointproxy/proxy_kubernetes.go | 73 + api/http/handler/endpoints/endpoint_create.go | 107 +- .../handler/endpoints/endpoint_snapshot.go | 12 +- .../handler/endpoints/endpoint_snapshots.go | 14 +- .../endpoints/endpoint_status_inspect.go | 2 +- api/http/handler/endpoints/endpoint_update.go | 7 +- api/http/handler/endpoints/handler.go | 6 +- api/http/handler/handler.go | 2 + api/http/handler/settings/handler.go | 6 +- .../handler/stacks/create_kubernetes_stack.go | 58 + api/http/handler/stacks/handler.go | 3 +- api/http/handler/stacks/stack_create.go | 6 + api/http/handler/tags/tag_delete.go | 2 +- api/http/handler/websocket/attach.go | 2 +- api/http/handler/websocket/exec.go | 2 +- api/http/handler/websocket/handler.go | 16 +- api/http/handler/websocket/hijack.go | 7 +- api/http/handler/websocket/pod.go | 116 + api/http/handler/websocket/proxy.go | 4 +- api/http/handler/websocket/stream.go | 18 +- api/http/proxy/factory/docker.go | 2 +- api/http/proxy/factory/docker/transport.go | 2 +- api/http/proxy/factory/factory.go | 30 +- api/http/proxy/factory/kubernetes.go | 109 + api/http/proxy/factory/kubernetes/token.go | 79 + .../proxy/factory/kubernetes/token_cache.go | 69 + .../proxy/factory/kubernetes/transport.go | 156 ++ api/http/proxy/manager.go | 12 +- api/http/security/bouncer.go | 2 +- api/http/server.go | 75 +- api/internal/edge/edgegroup.go | 2 +- api/internal/snapshot/snapshot.go | 72 +- api/kubernetes.go | 11 + api/kubernetes/cli/access.go | 86 + api/kubernetes/cli/client.go | 145 + api/kubernetes/cli/exec.go | 57 + api/kubernetes/cli/naming.go | 26 + api/kubernetes/cli/role.go | 33 + api/kubernetes/cli/secret.go | 74 + api/kubernetes/cli/service_account.go | 182 ++ api/kubernetes/snapshot.go | 83 + api/libcompose/compose_stack.go | 2 +- api/portainer.go | 211 +- app/__module.js | 1 + app/assets/css/app.css | 54 +- app/assets/images/kubernetes_endpoint.png | Bin 0 -> 4079 bytes app/constants.js | 4 +- .../containerNetworksDatatableController.js | 137 +- .../containersDatatable.html | 3 + .../volumesNFSForm/volumesnfsForm.html | 9 +- app/docker/services/containerService.js | 2 +- .../containers/inspect/containerinspect.html | 2 +- .../edge-groups-selector.html | 10 +- .../edge-groups-selector.js | 4 +- app/edge/components/group-form/groupForm.html | 6 +- .../models/registryRepository.js | 1 + .../services/registryV2Service.js | 3 +- .../edit/registryRepositoryController.js | 2 +- app/index.html | 2 +- app/kubernetes/__module.js | 261 ++ .../pods-datatable/podsDatatable.html | 157 ++ .../pods-datatable/podsDatatable.js | 12 + .../applicationsDatatable.html | 193 ++ .../applicationsDatatable.js | 15 + .../applicationsDatatableController.js | 77 + .../applicationsPortsDatatable.html | 192 ++ .../applicationsPortsDatatable.js | 13 + .../applicationsPortsDatatableController.js | 103 + .../applicationsStacksDatatable.html | 183 ++ .../applicationsStacksDatatable.js | 14 + .../applicationsStacksDatatableController.js | 110 + .../configurationsDatatable.html | 162 ++ .../configurationsDatatable.js | 13 + .../configurationsDatatableController.js | 83 + .../events-datatable/eventsDatatable.html | 134 + .../events-datatable/eventsDatatable.js | 14 + .../integratedApplicationsDatatable.html | 127 + .../integratedApplicationsDatatable.js | 13 + .../nodeApplicationsDatatable.html | 156 ++ .../nodeApplicationsDatatable.js | 13 + .../nodeApplicationsDatatableController.js | 54 + .../nodes-datatable/nodesDatatable.html | 164 ++ .../nodes-datatable/nodesDatatable.js | 13 + .../resourcePoolApplicationsDatatable.html | 145 + .../resourcePoolApplicationsDatatable.js | 13 + ...urcePoolApplicationsDatatableController.js | 47 + .../resourcePoolsDatatable.html | 160 ++ .../resourcePoolsDatatable.js | 14 + .../resourcePoolsDatatableController.js | 77 + .../volumes-datatable/volumesDatatable.html | 192 ++ .../volumes-datatable/volumesDatatable.js | 14 + .../volumesDatatableController.js | 91 + .../feedback-panel/feedbackPanel.html | 9 + .../feedback-panel/feedbackPanel.js | 3 + .../kubernetesConfigurationData.html | 97 + .../kubernetesConfigurationData.js | 8 + .../kubernetesConfigurationDataController.js | 68 + .../kubernetesSidebarContent.html | 18 + .../kubernetesSidebarContent.js | 6 + .../resourceReservation.html | 32 + .../resourceReservation.js | 11 + .../resourceReservationController.js | 23 + .../components/view-header/viewHeader.html | 10 + .../components/view-header/viewHeader.js | 9 + .../components/view-loading/viewLoading.html | 8 + .../components/view-loading/viewLoading.js | 6 + .../yaml-inspector/yamlInspector.html | 7 + .../yaml-inspector/yamlInspector.js | 8 + .../yaml-inspector/yamlInspectorController.js | 17 + app/kubernetes/converters/application.js | 302 +++ app/kubernetes/converters/configMap.js | 81 + app/kubernetes/converters/configuration.js | 31 + app/kubernetes/converters/daemonSet.js | 79 + app/kubernetes/converters/deployment.js | 80 + app/kubernetes/converters/event.js | 15 + app/kubernetes/converters/namespace.js | 29 + app/kubernetes/converters/node.js | 65 + .../converters/persistentVolumeClaim.js | 68 + app/kubernetes/converters/pod.js | 32 + app/kubernetes/converters/resourcePool.js | 12 + app/kubernetes/converters/resourceQuota.js | 87 + app/kubernetes/converters/secret.js | 58 + app/kubernetes/converters/service.js | 88 + app/kubernetes/converters/statefulSet.js | 84 + app/kubernetes/converters/storageClass.js | 14 + app/kubernetes/converters/volume.js | 12 + app/kubernetes/filters/applicationFilters.js | 72 + .../filters/configurationFilters.js | 13 + app/kubernetes/filters/eventFilters.js | 16 + app/kubernetes/filters/filters.js | 11 + app/kubernetes/filters/nodeFilters.js | 35 + app/kubernetes/filters/podFilters.js | 84 + app/kubernetes/helpers/application/index.js | 273 ++ .../helpers/application/rollback.js | 76 + app/kubernetes/helpers/commonHelper.js | 12 + app/kubernetes/helpers/configMapHelper.js | 36 + app/kubernetes/helpers/configurationHelper.js | 40 + app/kubernetes/helpers/eventHelper.js | 10 + .../helpers/formValidationHelper.js | 15 + app/kubernetes/helpers/history/daemonset.js | 27 + app/kubernetes/helpers/history/deployment.js | 56 + app/kubernetes/helpers/history/index.js | 50 + app/kubernetes/helpers/history/statefulset.js | 27 + app/kubernetes/helpers/namespaceHelper.js | 15 + app/kubernetes/helpers/resourceQuotaHelper.js | 9 + .../helpers/resourceReservationHelper.js | 44 + app/kubernetes/helpers/serviceHelper.js | 13 + app/kubernetes/helpers/stackHelper.js | 26 + app/kubernetes/helpers/volumeHelper.js | 37 + .../horizontal-pod-auto-scaler/converter.js | 23 + .../horizontal-pod-auto-scaler/helper.js | 26 + .../horizontal-pod-auto-scaler/models.js | 23 + .../horizontal-pod-auto-scaler/rest.js | 50 + .../horizontal-pod-auto-scaler/service.js | 135 + .../models/application/formValues.js | 118 + app/kubernetes/models/application/models.js | 114 + app/kubernetes/models/application/payloads.js | 142 + app/kubernetes/models/common/params.js | 11 + app/kubernetes/models/common/payloads.js | 15 + app/kubernetes/models/config-map/models.js | 21 + app/kubernetes/models/config-map/payloads.js | 27 + .../models/configuration/formvalues.js | 35 + app/kubernetes/models/configuration/models.js | 27 + app/kubernetes/models/daemon-set/models.js | 24 + app/kubernetes/models/daemon-set/payloads.js | 50 + app/kubernetes/models/deploy.js | 4 + app/kubernetes/models/deployment/models.js | 25 + app/kubernetes/models/deployment/payloads.js | 51 + app/kubernetes/models/event/models.js | 16 + app/kubernetes/models/history/models.js | 32 + app/kubernetes/models/namespace/models.js | 18 + app/kubernetes/models/namespace/payloads.js | 14 + app/kubernetes/models/node/models.js | 40 + app/kubernetes/models/pod/models.js | 21 + app/kubernetes/models/port/models.js | 33 + app/kubernetes/models/resource-pool/models.js | 20 + .../models/resource-quota/models.js | 34 + .../models/resource-quota/payloads.js | 43 + .../models/resource-reservation/models.js | 13 + app/kubernetes/models/secret/models.js | 18 + app/kubernetes/models/secret/payloads.js | 31 + app/kubernetes/models/service/models.js | 45 + app/kubernetes/models/service/payloads.js | 22 + app/kubernetes/models/stack/models.js | 14 + app/kubernetes/models/stateful-set/models.js | 27 + .../models/stateful-set/payloads.js | 52 + app/kubernetes/models/storage-class/models.js | 33 + app/kubernetes/models/volume/models.js | 39 + app/kubernetes/models/volume/payloads.js | 23 + app/kubernetes/rest/configMap.js | 38 + app/kubernetes/rest/controllerRevision.js | 44 + app/kubernetes/rest/daemonSet.js | 50 + app/kubernetes/rest/deployment.js | 50 + app/kubernetes/rest/event.js | 38 + app/kubernetes/rest/health.js | 17 + app/kubernetes/rest/namespace.js | 42 + app/kubernetes/rest/node.js | 37 + app/kubernetes/rest/persistentVolumeClaim.js | 44 + app/kubernetes/rest/pod.js | 44 + app/kubernetes/rest/replicaSet.js | 44 + app/kubernetes/rest/resourceQuota.js | 38 + app/kubernetes/rest/response/transform.js | 7 + app/kubernetes/rest/secret.js | 38 + app/kubernetes/rest/service.js | 44 + app/kubernetes/rest/statefulSet.js | 50 + app/kubernetes/rest/storage.js | 37 + app/kubernetes/services/applicationService.js | 343 +++ app/kubernetes/services/configMapService.js | 115 + .../services/configurationService.js | 128 + .../services/controllerRevisionService.js | 31 + app/kubernetes/services/daemonSetService.js | 133 + app/kubernetes/services/deploymentService.js | 133 + app/kubernetes/services/eventService.js | 34 + app/kubernetes/services/healthService.js | 30 + app/kubernetes/services/historyService.js | 54 + app/kubernetes/services/namespaceService.js | 96 + app/kubernetes/services/nodeService.js | 50 + .../services/persistentVolumeClaimService.js | 115 + app/kubernetes/services/podService.js | 75 + app/kubernetes/services/replicaSetService.js | 31 + .../services/resourcePoolService.js | 113 + .../services/resourceQuotaService.js | 109 + app/kubernetes/services/secretService.js | 110 + app/kubernetes/services/serviceService.js | 115 + app/kubernetes/services/stackService.js | 32 + app/kubernetes/services/statefulSetService.js | 144 + app/kubernetes/services/storageService.js | 37 + app/kubernetes/services/volumeService.js | 70 + .../views/applications/applications.html | 60 + .../views/applications/applications.js | 5 + .../applications/applicationsController.js | 139 + .../views/applications/console/console.html | 71 + .../views/applications/console/console.js | 8 + .../applications/console/consoleController.js | 113 + .../create/createApplication.html | 904 ++++++ .../applications/create/createApplication.js | 8 + .../create/createApplicationController.js | 627 +++++ .../views/applications/edit/application.html | 477 ++++ .../views/applications/edit/application.js | 8 + .../edit/applicationController.js | 245 ++ .../views/applications/logs/logs.html | 62 + .../views/applications/logs/logs.js | 8 + .../views/applications/logs/logsController.js | 87 + app/kubernetes/views/cluster/cluster.html | 40 + app/kubernetes/views/cluster/cluster.js | 5 + .../views/cluster/clusterController.js | 88 + app/kubernetes/views/cluster/node/node.html | 107 + app/kubernetes/views/cluster/node/node.js | 8 + .../views/cluster/node/nodeController.js | 137 + .../views/configurations/configurations.html | 19 + .../views/configurations/configurations.js | 5 + .../configurationsController.js | 105 + .../create/createConfiguration.html | 135 + .../create/createConfiguration.js | 5 + .../create/createConfigurationController.js | 93 + .../configurations/edit/configuration.html | 120 + .../configurations/edit/configuration.js | 8 + .../edit/configurationController.js | 236 ++ app/kubernetes/views/configure/configure.html | 122 + .../views/configure/configureController.js | 115 + app/kubernetes/views/dashboard/dashboard.html | 99 + app/kubernetes/views/dashboard/dashboard.js | 5 + .../views/dashboard/dashboardController.js | 88 + app/kubernetes/views/deploy/deploy.html | 118 + app/kubernetes/views/deploy/deploy.js | 5 + .../views/deploy/deployController.js | 99 + .../access/resourcePoolAccess.html | 116 + .../access/resourcePoolAccess.js | 8 + .../access/resourcePoolAccessController.js | 138 + .../create/createResourcePool.html | 163 ++ .../create/createResourcePool.js | 5 + .../create/createResourcePoolController.js | 127 + .../resource-pools/edit/resourcePool.html | 189 ++ .../views/resource-pools/edit/resourcePool.js | 8 + .../edit/resourcePoolController.js | 230 ++ .../views/resource-pools/resourcePools.html | 19 + .../views/resource-pools/resourcePools.js | 5 + .../resource-pools/resourcePoolsController.js | 77 + app/kubernetes/views/stacks/logs/logs.html | 60 + app/kubernetes/views/stacks/logs/logs.js | 8 + .../views/stacks/logs/logsController.js | 122 + app/kubernetes/views/volumes/edit/volume.html | 99 + app/kubernetes/views/volumes/edit/volume.js | 8 + .../views/volumes/edit/volumeController.js | 130 + app/kubernetes/views/volumes/volumes.html | 20 + app/kubernetes/views/volumes/volumes.js | 5 + .../views/volumes/volumesController.js | 82 + app/portainer/__module.js | 36 +- .../access-datatable/accessDatatable.html | 8 +- .../accessDatatableController.js | 2 +- .../porAccessControlPanelController.js | 2 +- .../accessManagement/porAccessManagement.html | 2 - .../porAccessManagementController.js | 35 +- .../datatables/genericDatatableController.js | 1 + .../endpoint-item/endpointItem.html | 37 +- app/portainer/components/header-content.js | 2 +- .../components/slider/sliderController.js | 64 +- app/portainer/components/tooltip.js | 7 +- app/portainer/error.js | 6 + app/portainer/filters/filters.js | 8 +- app/portainer/models/endpoint/formValues.js | 41 + app/portainer/models/endpoint/models.js | 28 + app/portainer/rest/auth.js | 8 +- app/portainer/services/allSettled.js | 34 + app/portainer/services/api/accessService.js | 37 +- app/portainer/services/api/endpointService.js | 24 +- app/portainer/services/api/registryService.js | 2 +- app/portainer/services/api/stackService.js | 24 +- app/portainer/services/api/tagService.js | 12 +- app/portainer/services/authentication.js | 10 +- app/portainer/services/localStorage.js | 7 + app/portainer/services/modalService.js | 15 + app/portainer/services/notifications.js | 6 +- app/portainer/services/stateManager.js | 6 + .../create/createEndpointController.js | 53 +- .../endpoints/create/createendpoint.html | 63 +- .../views/endpoints/edit/endpoint.html | 42 +- .../endpoints/edit/endpointController.js | 43 +- app/portainer/views/home/home.html | 2 + app/portainer/views/home/homeController.js | 38 +- .../views/init/endpoint/includes/agent.html | 34 + .../views/init/endpoint/includes/azure.html | 63 + .../init/endpoint/includes/localDocker.html | 21 + .../endpoint/includes/localKubernetes.html | 12 + .../views/init/endpoint/includes/remote.html | 120 + .../views/init/endpoint/initEndpoint.html | 401 +-- .../init/endpoint/initEndpointController.js | 304 ++- app/portainer/views/logout/logout.html | 16 + .../views/logout/logoutController.js | 61 + app/portainer/views/sidebar/sidebar.html | 4 +- app/portainer/views/teams/edit/team.html | 4 +- app/vendors.js | 4 +- build/download_kompose_binary.sh | 10 + build/download_kubectl_binary.sh | 10 + gruntfile.js | 69 +- jsconfig.json | 1 + package.json | 9 +- webpack/webpack.common.js | 1 + yarn.lock | 2412 ++++++++--------- 361 files changed, 20794 insertions(+), 2349 deletions(-) delete mode 100644 api/docker/snapshotter.go create mode 100644 api/exec/kubernetes_deploy.go create mode 100644 api/http/handler/auth/logout.go create mode 100644 api/http/handler/endpointproxy/proxy_kubernetes.go create mode 100644 api/http/handler/stacks/create_kubernetes_stack.go create mode 100644 api/http/handler/websocket/pod.go create mode 100644 api/http/proxy/factory/kubernetes.go create mode 100644 api/http/proxy/factory/kubernetes/token.go create mode 100644 api/http/proxy/factory/kubernetes/token_cache.go create mode 100644 api/http/proxy/factory/kubernetes/transport.go create mode 100644 api/kubernetes.go create mode 100644 api/kubernetes/cli/access.go create mode 100644 api/kubernetes/cli/client.go create mode 100644 api/kubernetes/cli/exec.go create mode 100644 api/kubernetes/cli/naming.go create mode 100644 api/kubernetes/cli/role.go create mode 100644 api/kubernetes/cli/secret.go create mode 100644 api/kubernetes/cli/service_account.go create mode 100644 api/kubernetes/snapshot.go create mode 100644 app/assets/images/kubernetes_endpoint.png create mode 100644 app/kubernetes/__module.js create mode 100644 app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html create mode 100644 app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/events-datatable/eventsDatatable.html create mode 100644 app/kubernetes/components/datatables/events-datatable/eventsDatatable.js create mode 100644 app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html create mode 100644 app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js create mode 100644 app/kubernetes/components/feedback-panel/feedbackPanel.html create mode 100644 app/kubernetes/components/feedback-panel/feedbackPanel.js create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js create mode 100644 app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html create mode 100644 app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js create mode 100644 app/kubernetes/components/resource-reservation/resourceReservation.html create mode 100644 app/kubernetes/components/resource-reservation/resourceReservation.js create mode 100644 app/kubernetes/components/resource-reservation/resourceReservationController.js create mode 100644 app/kubernetes/components/view-header/viewHeader.html create mode 100644 app/kubernetes/components/view-header/viewHeader.js create mode 100644 app/kubernetes/components/view-loading/viewLoading.html create mode 100644 app/kubernetes/components/view-loading/viewLoading.js create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspector.html create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspector.js create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspectorController.js create mode 100644 app/kubernetes/converters/application.js create mode 100644 app/kubernetes/converters/configMap.js create mode 100644 app/kubernetes/converters/configuration.js create mode 100644 app/kubernetes/converters/daemonSet.js create mode 100644 app/kubernetes/converters/deployment.js create mode 100644 app/kubernetes/converters/event.js create mode 100644 app/kubernetes/converters/namespace.js create mode 100644 app/kubernetes/converters/node.js create mode 100644 app/kubernetes/converters/persistentVolumeClaim.js create mode 100644 app/kubernetes/converters/pod.js create mode 100644 app/kubernetes/converters/resourcePool.js create mode 100644 app/kubernetes/converters/resourceQuota.js create mode 100644 app/kubernetes/converters/secret.js create mode 100644 app/kubernetes/converters/service.js create mode 100644 app/kubernetes/converters/statefulSet.js create mode 100644 app/kubernetes/converters/storageClass.js create mode 100644 app/kubernetes/converters/volume.js create mode 100644 app/kubernetes/filters/applicationFilters.js create mode 100644 app/kubernetes/filters/configurationFilters.js create mode 100644 app/kubernetes/filters/eventFilters.js create mode 100644 app/kubernetes/filters/filters.js create mode 100644 app/kubernetes/filters/nodeFilters.js create mode 100644 app/kubernetes/filters/podFilters.js create mode 100644 app/kubernetes/helpers/application/index.js create mode 100644 app/kubernetes/helpers/application/rollback.js create mode 100644 app/kubernetes/helpers/commonHelper.js create mode 100644 app/kubernetes/helpers/configMapHelper.js create mode 100644 app/kubernetes/helpers/configurationHelper.js create mode 100644 app/kubernetes/helpers/eventHelper.js create mode 100644 app/kubernetes/helpers/formValidationHelper.js create mode 100644 app/kubernetes/helpers/history/daemonset.js create mode 100644 app/kubernetes/helpers/history/deployment.js create mode 100644 app/kubernetes/helpers/history/index.js create mode 100644 app/kubernetes/helpers/history/statefulset.js create mode 100644 app/kubernetes/helpers/namespaceHelper.js create mode 100644 app/kubernetes/helpers/resourceQuotaHelper.js create mode 100644 app/kubernetes/helpers/resourceReservationHelper.js create mode 100644 app/kubernetes/helpers/serviceHelper.js create mode 100644 app/kubernetes/helpers/stackHelper.js create mode 100644 app/kubernetes/helpers/volumeHelper.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/converter.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/helper.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/models.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/rest.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/service.js create mode 100644 app/kubernetes/models/application/formValues.js create mode 100644 app/kubernetes/models/application/models.js create mode 100644 app/kubernetes/models/application/payloads.js create mode 100644 app/kubernetes/models/common/params.js create mode 100644 app/kubernetes/models/common/payloads.js create mode 100644 app/kubernetes/models/config-map/models.js create mode 100644 app/kubernetes/models/config-map/payloads.js create mode 100644 app/kubernetes/models/configuration/formvalues.js create mode 100644 app/kubernetes/models/configuration/models.js create mode 100644 app/kubernetes/models/daemon-set/models.js create mode 100644 app/kubernetes/models/daemon-set/payloads.js create mode 100644 app/kubernetes/models/deploy.js create mode 100644 app/kubernetes/models/deployment/models.js create mode 100644 app/kubernetes/models/deployment/payloads.js create mode 100644 app/kubernetes/models/event/models.js create mode 100644 app/kubernetes/models/history/models.js create mode 100644 app/kubernetes/models/namespace/models.js create mode 100644 app/kubernetes/models/namespace/payloads.js create mode 100644 app/kubernetes/models/node/models.js create mode 100644 app/kubernetes/models/pod/models.js create mode 100644 app/kubernetes/models/port/models.js create mode 100644 app/kubernetes/models/resource-pool/models.js create mode 100644 app/kubernetes/models/resource-quota/models.js create mode 100644 app/kubernetes/models/resource-quota/payloads.js create mode 100644 app/kubernetes/models/resource-reservation/models.js create mode 100644 app/kubernetes/models/secret/models.js create mode 100644 app/kubernetes/models/secret/payloads.js create mode 100644 app/kubernetes/models/service/models.js create mode 100644 app/kubernetes/models/service/payloads.js create mode 100644 app/kubernetes/models/stack/models.js create mode 100644 app/kubernetes/models/stateful-set/models.js create mode 100644 app/kubernetes/models/stateful-set/payloads.js create mode 100644 app/kubernetes/models/storage-class/models.js create mode 100644 app/kubernetes/models/volume/models.js create mode 100644 app/kubernetes/models/volume/payloads.js create mode 100644 app/kubernetes/rest/configMap.js create mode 100644 app/kubernetes/rest/controllerRevision.js create mode 100644 app/kubernetes/rest/daemonSet.js create mode 100644 app/kubernetes/rest/deployment.js create mode 100644 app/kubernetes/rest/event.js create mode 100644 app/kubernetes/rest/health.js create mode 100644 app/kubernetes/rest/namespace.js create mode 100644 app/kubernetes/rest/node.js create mode 100644 app/kubernetes/rest/persistentVolumeClaim.js create mode 100644 app/kubernetes/rest/pod.js create mode 100644 app/kubernetes/rest/replicaSet.js create mode 100644 app/kubernetes/rest/resourceQuota.js create mode 100644 app/kubernetes/rest/response/transform.js create mode 100644 app/kubernetes/rest/secret.js create mode 100644 app/kubernetes/rest/service.js create mode 100644 app/kubernetes/rest/statefulSet.js create mode 100644 app/kubernetes/rest/storage.js create mode 100644 app/kubernetes/services/applicationService.js create mode 100644 app/kubernetes/services/configMapService.js create mode 100644 app/kubernetes/services/configurationService.js create mode 100644 app/kubernetes/services/controllerRevisionService.js create mode 100644 app/kubernetes/services/daemonSetService.js create mode 100644 app/kubernetes/services/deploymentService.js create mode 100644 app/kubernetes/services/eventService.js create mode 100644 app/kubernetes/services/healthService.js create mode 100644 app/kubernetes/services/historyService.js create mode 100644 app/kubernetes/services/namespaceService.js create mode 100644 app/kubernetes/services/nodeService.js create mode 100644 app/kubernetes/services/persistentVolumeClaimService.js create mode 100644 app/kubernetes/services/podService.js create mode 100644 app/kubernetes/services/replicaSetService.js create mode 100644 app/kubernetes/services/resourcePoolService.js create mode 100644 app/kubernetes/services/resourceQuotaService.js create mode 100644 app/kubernetes/services/secretService.js create mode 100644 app/kubernetes/services/serviceService.js create mode 100644 app/kubernetes/services/stackService.js create mode 100644 app/kubernetes/services/statefulSetService.js create mode 100644 app/kubernetes/services/storageService.js create mode 100644 app/kubernetes/services/volumeService.js create mode 100644 app/kubernetes/views/applications/applications.html create mode 100644 app/kubernetes/views/applications/applications.js create mode 100644 app/kubernetes/views/applications/applicationsController.js create mode 100644 app/kubernetes/views/applications/console/console.html create mode 100644 app/kubernetes/views/applications/console/console.js create mode 100644 app/kubernetes/views/applications/console/consoleController.js create mode 100644 app/kubernetes/views/applications/create/createApplication.html create mode 100644 app/kubernetes/views/applications/create/createApplication.js create mode 100644 app/kubernetes/views/applications/create/createApplicationController.js create mode 100644 app/kubernetes/views/applications/edit/application.html create mode 100644 app/kubernetes/views/applications/edit/application.js create mode 100644 app/kubernetes/views/applications/edit/applicationController.js create mode 100644 app/kubernetes/views/applications/logs/logs.html create mode 100644 app/kubernetes/views/applications/logs/logs.js create mode 100644 app/kubernetes/views/applications/logs/logsController.js create mode 100644 app/kubernetes/views/cluster/cluster.html create mode 100644 app/kubernetes/views/cluster/cluster.js create mode 100644 app/kubernetes/views/cluster/clusterController.js create mode 100644 app/kubernetes/views/cluster/node/node.html create mode 100644 app/kubernetes/views/cluster/node/node.js create mode 100644 app/kubernetes/views/cluster/node/nodeController.js create mode 100644 app/kubernetes/views/configurations/configurations.html create mode 100644 app/kubernetes/views/configurations/configurations.js create mode 100644 app/kubernetes/views/configurations/configurationsController.js create mode 100644 app/kubernetes/views/configurations/create/createConfiguration.html create mode 100644 app/kubernetes/views/configurations/create/createConfiguration.js create mode 100644 app/kubernetes/views/configurations/create/createConfigurationController.js create mode 100644 app/kubernetes/views/configurations/edit/configuration.html create mode 100644 app/kubernetes/views/configurations/edit/configuration.js create mode 100644 app/kubernetes/views/configurations/edit/configurationController.js create mode 100644 app/kubernetes/views/configure/configure.html create mode 100644 app/kubernetes/views/configure/configureController.js create mode 100644 app/kubernetes/views/dashboard/dashboard.html create mode 100644 app/kubernetes/views/dashboard/dashboard.js create mode 100644 app/kubernetes/views/dashboard/dashboardController.js create mode 100644 app/kubernetes/views/deploy/deploy.html create mode 100644 app/kubernetes/views/deploy/deploy.js create mode 100644 app/kubernetes/views/deploy/deployController.js create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccess.html create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccess.js create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePool.html create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePool.js create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePoolController.js create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePool.html create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePool.js create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePoolController.js create mode 100644 app/kubernetes/views/resource-pools/resourcePools.html create mode 100644 app/kubernetes/views/resource-pools/resourcePools.js create mode 100644 app/kubernetes/views/resource-pools/resourcePoolsController.js create mode 100644 app/kubernetes/views/stacks/logs/logs.html create mode 100644 app/kubernetes/views/stacks/logs/logs.js create mode 100644 app/kubernetes/views/stacks/logs/logsController.js create mode 100644 app/kubernetes/views/volumes/edit/volume.html create mode 100644 app/kubernetes/views/volumes/edit/volume.js create mode 100644 app/kubernetes/views/volumes/edit/volumeController.js create mode 100644 app/kubernetes/views/volumes/volumes.html create mode 100644 app/kubernetes/views/volumes/volumes.js create mode 100644 app/kubernetes/views/volumes/volumesController.js create mode 100644 app/portainer/error.js create mode 100644 app/portainer/models/endpoint/formValues.js create mode 100644 app/portainer/models/endpoint/models.js create mode 100644 app/portainer/services/allSettled.js create mode 100644 app/portainer/views/init/endpoint/includes/agent.html create mode 100644 app/portainer/views/init/endpoint/includes/azure.html create mode 100644 app/portainer/views/init/endpoint/includes/localDocker.html create mode 100644 app/portainer/views/init/endpoint/includes/localKubernetes.html create mode 100644 app/portainer/views/init/endpoint/includes/remote.html create mode 100644 app/portainer/views/logout/logout.html create mode 100644 app/portainer/views/logout/logoutController.js create mode 100755 build/download_kompose_binary.sh create mode 100755 build/download_kubectl_binary.sh diff --git a/.codeclimate.yml b/.codeclimate.yml index 07eb34e8b..32136dcac 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,62 +1,44 @@ version: "2" checks: argument-count: - enabled: true - config: - threshold: 4 + enabled: false complex-logic: - enabled: true - config: - threshold: 4 + enabled: false file-lines: - enabled: true - config: - threshold: 300 + enabled: false method-complexity: enabled: false method-count: - enabled: true - config: - threshold: 20 + enabled: false method-lines: - enabled: true - config: - threshold: 50 + enabled: false nested-control-flow: - enabled: true - config: - threshold: 4 + enabled: false return-statements: enabled: false similar-code: - enabled: true - config: - threshold: #language-specific defaults. overrides affect all languages. + enabled: false identical-code: - enabled: true - config: - threshold: #language-specific defaults. overrides affect all languages. + enabled: false plugins: gofmt: enabled: true - golint: - enabled: true - govet: - enabled: true - csslint: - enabled: true - duplication: - enabled: true - config: - languages: - javascript: - mass_threshold: 80 eslint: enabled: true channel: "eslint-5" config: config: .eslintrc.yml - fixme: - enabled: true exclude_patterns: +- assets/ +- build/ +- dist/ +- distribution/ +- node_modules - test/ +- webpack/ +- gruntfile.js +- webpack.config.js +- api/ +- "!app/kubernetes/**" +- .github/ +- .tmp/ diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 9005ccd40..00b13e087 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,47 +1,48 @@ ---- -name: Bug report -about: Create a bug report - ---- - - - -**Bug description** -A clear and concise description of what the bug is. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Portainer Logs** -Provide the logs of your Portainer container or Service. -You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer) - -**Steps to reproduce the issue:** -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Technical details:** -* Portainer version: -* Docker version (managed by Portainer): -* Platform (windows/linux): -* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): -* Browser: - -**Additional context** -Add any other context about the problem here. +--- +name: Bug report +about: Create a bug report +--- + + + +**Bug description** +A clear and concise description of what the bug is. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Portainer Logs** +Provide the logs of your Portainer container or Service. +You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer) + +**Steps to reproduce the issue:** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Technical details:** + +- Portainer version: +- Docker version (managed by Portainer): +- Platform (windows/linux): +- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): +- Browser: + +**Additional context** +Add any other context about the problem here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3f4d5a37..537ae511f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,10 @@ Some basic conventions for contributing to this project. Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. -* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring -* Develop in a topic branch, not master/develop +- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring +- Develop in a topic branch, not master/develop -When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator). +When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator). For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`. @@ -37,14 +37,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to Must be one of the following: -* **feat**: A new feature -* **fix**: A bug fix -* **docs**: Documentation only changes -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **refactor**: A code change that neither fixes a bug or adds a feature -* **test**: Adding missing tests -* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation +- **refactor**: A code change that neither fixes a bug or adds a feature +- **test**: Adding missing tests +- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation ### Scope @@ -57,9 +57,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta The subject contains succinct description of the change: -* use the imperative, present tense: "change" not "changed" nor "changes" -* don't capitalize first letter -* no dot (.) at the end +- use the imperative, present tense: "change" not "changed" nor "changes" +- don't capitalize first letter +- no dot (.) at the end ## Contribution process diff --git a/api/chisel/service.go b/api/chisel/service.go index e21b67358..12ea9ef31 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -28,7 +28,7 @@ type Service struct { serverPort string tunnelDetailsMap cmap.ConcurrentMap dataStore portainer.DataStore - snapshotter portainer.Snapshotter + snapshotService portainer.SnapshotService chiselServer *chserver.Server } @@ -45,7 +45,7 @@ func NewService(dataStore portainer.DataStore) *Service { // be found inside the database, it will generate a new one randomly and persist it. // It starts the tunnel status verification process in the background. // The snapshotter is used in the tunnel status verification process. -func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error { +func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error { keySeed, err := service.retrievePrivateKeySeed() if err != nil { return err @@ -78,7 +78,7 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotter portain return err } - service.snapshotter = snapshotter + service.snapshotService = snapshotService go service.startTunnelVerificationLoop() return nil @@ -177,13 +177,13 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun } endpointURL := endpoint.URL + endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort) - snapshot, err := service.snapshotter.CreateSnapshot(endpoint) + err = service.snapshotService.SnapshotEndpoint(endpoint) if err != nil { return err } - endpoint.Snapshots = []portainer.Snapshot{*snapshot} endpoint.URL = endpointURL return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 728018ccd..830a8aede 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -6,12 +6,9 @@ import ( "strings" "time" - "github.com/portainer/portainer/api/chisel" - "github.com/portainer/portainer/api/internal/authorization" - "github.com/portainer/portainer/api/internal/snapshot" - - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" + "github.com/portainer/portainer/api/chisel" "github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/docker" @@ -20,7 +17,11 @@ import ( "github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/http" "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/internal/authorization" + "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/jwt" + "github.com/portainer/portainer/api/kubernetes" + kubecli "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/libcompose" ) @@ -78,6 +79,10 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService) } +func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer { + return exec.NewKubernetesDeployer(assetsPath) +} + func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) { settings, err := dataStore.Settings().Settings() if err != nil { @@ -107,12 +112,24 @@ func initGitService() portainer.GitService { return git.NewService() } -func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { +func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { return docker.NewClientFactory(signatureService, reverseTunnelService) } -func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter { - return docker.NewSnapshotter(clientFactory) +func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *kubecli.ClientFactory { + return kubecli.NewClientFactory(signatureService, reverseTunnelService) +} + +func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) { + dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory) + kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory) + + snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter) + if err != nil { + return nil, err + } + + return snapshotService, nil } func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error { @@ -187,7 +204,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D return generateAndStoreKeyPair(fileService, signatureService) } -func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { tlsConfiguration := portainer.TLSConfiguration{ TLS: *flags.TLS, TLSSkipVerify: *flags.TLSSkipVerify, @@ -214,7 +231,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat Extensions: []portainer.EndpointExtension{}, TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -233,10 +251,15 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat } } - return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) + err := snapshotService.SnapshotEndpoint(endpoint) + if err != nil { + log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + } + + return dataStore.Endpoint().CreateEndpoint(endpoint) } -func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { if strings.HasPrefix(endpointURL, "tcp://") { _, err := client.ExecutePingOperation(endpointURL, nil) if err != nil { @@ -257,27 +280,19 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, Extensions: []portainer.EndpointExtension{}, TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } - return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) -} - -func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { - snapshot, err := snapshotter.CreateSnapshot(endpoint) - endpoint.Status = portainer.EndpointStatusUp + err := snapshotService.SnapshotEndpoint(endpoint) if err != nil { log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) } - if snapshot != nil { - endpoint.Snapshots = []portainer.Snapshot{*snapshot} - } - return dataStore.Endpoint().CreateEndpoint(endpoint) } -func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { if *flags.EndpointURL == "" { return nil } @@ -293,9 +308,9 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap } if *flags.TLS || *flags.TLSSkipVerify { - return createTLSSecuredEndpoint(flags, dataStore, snapshotter) + return createTLSSecuredEndpoint(flags, dataStore, snapshotService) } - return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotter) + return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService) } func initExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) (portainer.ExtensionManager, error) { @@ -357,11 +372,10 @@ func main() { reverseTunnelService := chisel.NewService(dataStore) - clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService) + dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) + kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService) - snapshotter := initSnapshotter(clientFactory) - - snapshotService, err := snapshot.NewService(*flags.SnapshotInterval, dataStore, snapshotter) + snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory) if err != nil { log.Fatal(err) } @@ -374,6 +388,8 @@ func main() { composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) + kubernetesDeployer := initKubernetesDeployer(*flags.Assets) + if dataStore.IsNew() { err = updateSettingsFromFlags(dataStore, flags) if err != nil { @@ -388,7 +404,7 @@ func main() { applicationStatus := initStatus(flags) - err = initEndpoint(flags, dataStore, snapshotter) + err = initEndpoint(flags, dataStore, snapshotService) if err != nil { log.Fatal(err) } @@ -432,32 +448,33 @@ func main() { go terminateIfNoAdminCreated(dataStore) - err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter) + err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService) if err != nil { log.Fatal(err) } var server portainer.Server = &http.Server{ - ReverseTunnelService: reverseTunnelService, - Status: applicationStatus, - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - DataStore: dataStore, - SwarmStackManager: swarmStackManager, - ComposeStackManager: composeStackManager, - ExtensionManager: extensionManager, - CryptoService: cryptoService, - JWTService: jwtService, - FileService: fileService, - LDAPService: ldapService, - GitService: gitService, - SignatureService: digitalSignatureService, - SnapshotService: snapshotService, - Snapshotter: snapshotter, - SSL: *flags.SSL, - SSLCert: *flags.SSLCert, - SSLKey: *flags.SSLKey, - DockerClientFactory: clientFactory, + ReverseTunnelService: reverseTunnelService, + Status: applicationStatus, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + DataStore: dataStore, + SwarmStackManager: swarmStackManager, + ComposeStackManager: composeStackManager, + KubernetesDeployer: kubernetesDeployer, + ExtensionManager: extensionManager, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + LDAPService: ldapService, + GitService: gitService, + SignatureService: digitalSignatureService, + SnapshotService: snapshotService, + SSL: *flags.SSL, + SSLCert: *flags.SSLCert, + SSLKey: *flags.SSLKey, + DockerClientFactory: dockerClientFactory, + KubernetesClientFactory: kubernetesClientFactory, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) diff --git a/api/docker/client.go b/api/docker/client.go index c1bd7a8d0..6ccb5ad7c 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -31,7 +31,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers } } -// CreateClient is a generic function to create a Docker client based on +// createClient is a generic function to create a Docker client based on // a specific endpoint configuration. The nodeName parameter can be used // with an agent enabled endpoint to target a specific node in an agent cluster. func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) { @@ -39,7 +39,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam return nil, unsupportedEnvironmentType } else if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) - } else if endpoint.Type == portainer.EdgeAgentEnvironment { + } else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName) } diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index 0ab887373..f9ead6d3e 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -12,13 +12,36 @@ import ( "github.com/portainer/portainer/api" ) -func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snapshot, error) { +// Snapshotter represents a service used to create endpoint snapshots +type Snapshotter struct { + clientFactory *ClientFactory +} + +// NewSnapshotter returns a new Snapshotter instance +func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter { + return &Snapshotter{ + clientFactory: clientFactory, + } +} + +// CreateSnapshot creates a snapshot of a specific Docker endpoint +func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) { + cli, err := snapshotter.clientFactory.CreateClient(endpoint, "") + if err != nil { + return nil, err + } + defer cli.Close() + + return snapshot(cli, endpoint) +} + +func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) { _, err := cli.Ping(context.Background()) if err != nil { return nil, err } - snapshot := &portainer.Snapshot{ + snapshot := &portainer.DockerSnapshot{ StackCount: 0, } @@ -68,7 +91,7 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snap return snapshot, nil } -func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotInfo(snapshot *portainer.DockerSnapshot, cli *client.Client) error { info, err := cli.Info(context.Background()) if err != nil { return err @@ -82,7 +105,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error { nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{}) if err != nil { return err @@ -98,7 +121,7 @@ func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Client) error { stacks := make(map[string]struct{}) services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{}) @@ -119,7 +142,7 @@ func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) err return nil } -func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error { containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true}) if err != nil { return err @@ -159,7 +182,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error return nil } -func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) error { images, err := cli.ImageList(context.Background(), types.ImageListOptions{}) if err != nil { return err @@ -170,7 +193,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) error { volumes, err := cli.VolumeList(context.Background(), filters.Args{}) if err != nil { return err @@ -181,7 +204,7 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotNetworks(snapshot *portainer.DockerSnapshot, cli *client.Client) error { networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{}) if err != nil { return err @@ -190,7 +213,7 @@ func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) error { version, err := cli.ServerVersion(context.Background()) if err != nil { return err diff --git a/api/docker/snapshotter.go b/api/docker/snapshotter.go deleted file mode 100644 index 25eceb023..000000000 --- a/api/docker/snapshotter.go +++ /dev/null @@ -1,28 +0,0 @@ -package docker - -import ( - "github.com/portainer/portainer/api" -) - -// Snapshotter represents a service used to create endpoint snapshots -type Snapshotter struct { - clientFactory *ClientFactory -} - -// NewSnapshotter returns a new Snapshotter instance -func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter { - return &Snapshotter{ - clientFactory: clientFactory, - } -} - -// CreateSnapshot creates a snapshot of a specific endpoint -func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) { - cli, err := snapshotter.clientFactory.CreateClient(endpoint, "") - if err != nil { - return nil, err - } - defer cli.Close() - - return snapshot(cli, endpoint) -} diff --git a/api/exec/kubernetes_deploy.go b/api/exec/kubernetes_deploy.go new file mode 100644 index 000000000..0330237e0 --- /dev/null +++ b/api/exec/kubernetes_deploy.go @@ -0,0 +1,89 @@ +package exec + +import ( + "bytes" + "errors" + "io/ioutil" + "os/exec" + "path" + "runtime" + "strings" + + portainer "github.com/portainer/portainer/api" +) + +// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment. +type KubernetesDeployer struct { + binaryPath string +} + +// NewKubernetesDeployer initializes a new KubernetesDeployer service. +func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer { + return &KubernetesDeployer{ + binaryPath: binaryPath, + } +} + +// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint. +// If composeFormat is set to true, it will leverage the kompose binary to deploy a compose compliant manifest. +// Otherwise it will use kubectl to deploy the manifest. +func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) { + if composeFormat { + convertedData, err := deployer.convertComposeData(data) + if err != nil { + return nil, err + } + data = string(convertedData) + } + + token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return nil, err + } + + command := path.Join(deployer.binaryPath, "kubectl") + if runtime.GOOS == "windows" { + command = path.Join(deployer.binaryPath, "kubectl.exe") + } + + args := make([]string, 0) + args = append(args, "--server", endpoint.URL) + args = append(args, "--insecure-skip-tls-verify") + args = append(args, "--token", string(token)) + args = append(args, "--namespace", namespace) + args = append(args, "apply", "-f", "-") + + var stderr bytes.Buffer + cmd := exec.Command(command, args...) + cmd.Stderr = &stderr + cmd.Stdin = strings.NewReader(data) + + output, err := cmd.Output() + if err != nil { + return nil, errors.New(stderr.String()) + } + + return output, nil +} + +func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) { + command := path.Join(deployer.binaryPath, "kompose") + if runtime.GOOS == "windows" { + command = path.Join(deployer.binaryPath, "kompose.exe") + } + + args := make([]string, 0) + args = append(args, "convert", "-f", "-", "--stdout") + + var stderr bytes.Buffer + cmd := exec.Command(command, args...) + cmd.Stderr = &stderr + cmd.Stdin = strings.NewReader(data) + + output, err := cmd.Output() + if err != nil { + return nil, errors.New(stderr.String()) + } + + return output, nil +} diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index d5b779a02..94d9e0904 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -121,7 +121,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa args = append(args, "--config", dataPath) endpointURL := endpoint.URL - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port) } diff --git a/api/go.mod b/api/go.mod index 3af5acf37..650af2de9 100644 --- a/api/go.mod +++ b/api/go.mod @@ -29,8 +29,12 @@ require ( github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 + golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/src-d/go-git.v4 v4.13.1 + k8s.io/api v0.17.2 + k8s.io/apimachinery v0.17.2 + k8s.io/client-go v0.17.2 ) replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 diff --git a/api/go.sum b/api/go.sum index 66b50d287..d7b7db557 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,6 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI= github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -8,6 +17,9 @@ github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+q github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= @@ -36,6 +48,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,14 +73,20 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= @@ -77,21 +96,42 @@ github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= @@ -101,6 +141,12 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -115,13 +161,17 @@ github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxF github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY= github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM= github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw= github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= @@ -134,6 +184,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -145,12 +196,22 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s= @@ -160,9 +221,11 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5X github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8= @@ -191,14 +254,20 @@ github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= @@ -210,30 +279,58 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -241,18 +338,36 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= @@ -260,17 +375,53 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s= +k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU= +k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= +k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ= +k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw= +k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= +k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 1f8415597..9bc98f834 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -7,6 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -14,12 +15,13 @@ import ( // Handler is the HTTP handler used to handle authentication operations. type Handler struct { *mux.Router - DataStore portainer.DataStore - CryptoService portainer.CryptoService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - ProxyManager *proxy.Manager - AuthorizationService *authorization.Service + DataStore portainer.DataStore + CryptoService portainer.CryptoService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + ProxyManager *proxy.Manager + AuthorizationService *authorization.Service + KubernetesTokenCacheManager *kubernetes.TokenCacheManager } // NewHandler creates a handler to manage authentication operations. @@ -32,6 +34,8 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost) h.Handle("/auth", rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost) + h.Handle("/auth/logout", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/auth/logout.go b/api/http/handler/auth/logout.go new file mode 100644 index 000000000..90519d5b9 --- /dev/null +++ b/api/http/handler/auth/logout.go @@ -0,0 +1,21 @@ +package auth + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api/http/security" +) + +// POST request on /logout +func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err} + } + + handler.KubernetesTokenCacheManager.RemoveUserFromCache(int(tokenData.ID)) + + return response.Empty(w) +} diff --git a/api/http/handler/edgegroups/associated_endpoints.go b/api/http/handler/edgegroups/associated_endpoints.go index 8eeed5620..b34711eda 100644 --- a/api/http/handler/edgegroups/associated_endpoints.go +++ b/api/http/handler/edgegroups/associated_endpoints.go @@ -38,7 +38,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc results := []portainer.EndpointID{} for _, endpoint := range endpoints { - if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment { + if _, ok := endpointSet[endpoint.ID]; ok && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) { results = append(results, endpoint.ID) } } diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index fc0d09d37..90c733073 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -67,7 +67,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { endpointIDs = append(endpointIDs, endpoint.ID) } } diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index c2c4e36d2..2be4fb346 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -87,7 +87,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { endpointIDs = append(endpointIDs, endpoint.ID) } } diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index 76720006c..33f91493a 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -187,7 +187,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file [] return err } - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { delete(edgeJob.Endpoints, ID) } } diff --git a/api/http/handler/edgejobs/edgejob_update.go b/api/http/handler/edgejobs/edgejob_update.go index b33ad67b8..559ba26f2 100644 --- a/api/http/handler/edgejobs/edgejob_update.go +++ b/api/http/handler/edgejobs/edgejob_update.go @@ -82,7 +82,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload * return err } - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { continue } diff --git a/api/http/handler/endpointgroups/endpoints.go b/api/http/handler/endpointgroups/endpoints.go index e854ca635..c1757d3c7 100644 --- a/api/http/handler/endpointgroups/endpoints.go +++ b/api/http/handler/endpointgroups/endpoints.go @@ -6,7 +6,7 @@ import ( ) func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return nil } diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 870f2de80..037cb4dfb 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -27,6 +27,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) + h.PathPrefix("/{id}/kubernetes").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI))) h.PathPrefix("/{id}/storidge").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) return h diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 8a228fcf4..041a6f178 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -30,7 +30,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { if endpoint.EdgeID == "" { return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} } diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go new file mode 100644 index 000000000..744ab4340 --- /dev/null +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -0,0 +1,73 @@ +package endpointproxy + +import ( + "errors" + "fmt" + "time" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/portainer/api" + + "net/http" +) + +func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + if endpoint.EdgeID == "" { + return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} + } + + tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) + if tunnel.Status == portainer.EdgeAgentIdle { + handler.ProxyManager.DeleteEndpointProxy(endpoint) + + err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err} + } + + settings, err := handler.DataStore.Settings().Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second + time.Sleep(waitForAgentToConnect * 2) + } + } + + var proxy http.Handler + proxy = handler.ProxyManager.GetEndpointProxy(endpoint) + if proxy == nil { + proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err} + } + } + + requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID) + if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + requestPrefix = fmt.Sprintf("/%d", endpointID) + } + + http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r) + return nil +} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a2b90c1cd..9b8692903 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -13,7 +13,6 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" ) @@ -118,11 +117,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.AzureAuthenticationKey = azureAuthenticationKey default: - url, err := request.RetrieveMultiPartFormValue(r, "URL", true) + endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { return portainer.Error("Invalid endpoint URL") } - payload.URL = url + payload.URL = endpointURL publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) payload.PublicURL = publicURL @@ -167,7 +166,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * EdgeStacks: map[portainer.EdgeStackID]bool{}, } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) for _, stackID := range relatedEdgeStacks { relationObject.EdgeStacks[stackID] = true @@ -183,14 +182,22 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { + switch portainer.EndpointType(payload.EndpointType) { + case portainer.AzureEnvironment: return handler.createAzureEndpoint(payload) - } else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { - return handler.createEdgeAgentEndpoint(payload) + + case portainer.EdgeAgentOnDockerEnvironment: + return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment) + + case portainer.KubernetesLocalEnvironment: + return handler.createKubernetesEndpoint(payload) + + case portainer.EdgeAgentOnKubernetesEnvironment: + return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment) } if payload.TLS { - return handler.createTLSSecuredEndpoint(payload) + return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType)) } return handler.createUnsecuredEndpoint(payload) } @@ -222,7 +229,8 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po AzureCredentials: credentials, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) @@ -233,8 +241,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po return endpoint, nil } -func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - endpointType := portainer.EdgeAgentEnvironment +func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() portainerURL, err := url.Parse(payload.URL) @@ -267,9 +274,10 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, EdgeKey: edgeKey, EdgeCheckinInterval: payload.EdgeCheckinInterval, + Kubernetes: portainer.KubernetesDefault(), } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) @@ -288,14 +296,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) if runtime.GOOS == "windows" { payload.URL = "npipe:////./pipe/docker_engine" } - } else { - agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} - } - if agentOnDockerEnvironment { - endpointType = portainer.AgentOnDockerEnvironment - } } endpointID := handler.DataStore.Endpoint().GetNextIdentifier() @@ -314,7 +314,8 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } err := handler.snapshotAndPersistEndpoint(endpoint) @@ -325,22 +326,42 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) return endpoint, nil } -func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipClientVerify, payload.TLSSkipVerify) +func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { + if payload.URL == "" { + payload.URL = "https://kubernetes.default.svc" + } + + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() + + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: payload.URL, + Type: portainer.KubernetesLocalEnvironment, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + TLSConfig: portainer.TLSConfiguration{ + TLS: payload.TLS, + TLSSkipVerify: payload.TLSSkipVerify, + }, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + TagIDs: payload.TagIDs, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), + } + + err := handler.snapshotAndPersistEndpoint(endpoint) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create TLS configuration", err} + return nil, err } - agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, tlsConfig) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} - } - - endpointType := portainer.DockerEnvironment - if agentOnDockerEnvironment { - endpointType = portainer.AgentOnDockerEnvironment - } + return endpoint, nil +} +func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), @@ -358,25 +379,25 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } - filesystemError := handler.storeTLSFiles(endpoint, payload) + err := handler.storeTLSFiles(endpoint, payload) if err != nil { - return nil, filesystemError + return nil, err } - endpointCreationError := handler.snapshotAndPersistEndpoint(endpoint) - if endpointCreationError != nil { - return nil, endpointCreationError + err = handler.snapshotAndPersistEndpoint(endpoint) + if err != nil { + return nil, err } return endpoint, nil } func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) *httperror.HandlerError { - snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint) - endpoint.Status = portainer.EndpointStatusUp + err := handler.SnapshotService.SnapshotEndpoint(endpoint) if err != nil { if strings.Contains(err.Error(), "Invalid request signature") { err = errors.New("agent already paired with another Portainer instance") @@ -384,10 +405,6 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err} } - if snapshot != nil { - endpoint.Snapshots = []portainer.Snapshot{*snapshot} - } - err = handler.saveEndpointAndUpdateAuthorizations(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 18182db17..03816b642 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/snapshot" ) // POST request on /api/endpoints/:id/snapshot @@ -23,11 +24,11 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if endpoint.Type == portainer.AzureEnvironment { - return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err} + if !snapshot.SupportDirectSnapshot(endpoint) { + return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this endpoint", err} } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) + snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -39,9 +40,8 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) latestEndpointReference.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 33d6f30d0..3fd83d071 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -7,6 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/snapshot" ) // POST request on /api/endpoints/snapshot @@ -17,11 +18,11 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request } for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment { + if !snapshot.SupportDirectSnapshot(&endpoint) { continue } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) + snapshotError := handler.SnapshotService.SnapshotEndpoint(&endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -29,15 +30,14 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request continue } - latestEndpointReference.Status = portainer.EndpointStatusUp + endpoint.Status = portainer.EndpointStatusUp if snapshotError != nil { log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) - latestEndpointReference.Status = portainer.EndpointStatusDown + endpoint.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index a48dac06d..b29a1da5b 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type stackStatusResponse struct { diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index bf73d9320..8979571b0 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -30,6 +30,7 @@ type endpointUpdatePayload struct { UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies EdgeCheckinInterval *int + Kubernetes *portainer.KubernetesData } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -120,6 +121,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } + if payload.Kubernetes != nil { + endpoint.Kubernetes = *payload.Kubernetes + } + updateAuthorizations := false if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { endpoint.UserAccessPolicies = payload.UserAccessPolicies @@ -227,7 +232,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - if endpoint.Type == portainer.EdgeAgentEnvironment && (groupIDChanged || tagsChanged) { + if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 6610d134c..3722c6e24 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -2,7 +2,7 @@ package endpoints import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -15,7 +15,7 @@ import ( func hideFields(endpoint *portainer.Endpoint) { endpoint.AzureCredentials = portainer.AzureCredentials{} if len(endpoint.Snapshots) > 0 { - endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{} + endpoint.Snapshots[0].SnapshotRaw = portainer.DockerSnapshotRaw{} } } @@ -28,7 +28,7 @@ type Handler struct { FileService portainer.FileService ProxyManager *proxy.Manager ReverseTunnelService portainer.ReverseTunnelService - Snapshotter portainer.Snapshotter + SnapshotService portainer.SnapshotService } // NewHandler creates a handler to manage endpoint operations. diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 8dd9c3492..113fa60c4 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -87,6 +87,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/docker/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) + case strings.Contains(r.URL.Path, "/kubernetes/"): + http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/storidge/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/azure/"): diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index c4471a2c5..143f99962 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -1,14 +1,14 @@ package settings import ( - "github.com/portainer/portainer/api/internal/authorization" "net/http" + "github.com/portainer/portainer/api/internal/authorization" + "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" - "github.com/portainer/portainer/api/internal/snapshot" ) func hideFields(settings *portainer.Settings) { @@ -24,7 +24,7 @@ type Handler struct { FileService portainer.FileService JWTService portainer.JWTService LDAPService portainer.LDAPService - SnapshotService *snapshot.Service + SnapshotService portainer.SnapshotService } // NewHandler creates a handler to manage settings operations. diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go new file mode 100644 index 000000000..cbf8eb1fe --- /dev/null +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -0,0 +1,58 @@ +package stacks + +import ( + "net/http" + + "github.com/asaskevich/govalidator" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" +) + +type kubernetesStackPayload struct { + ComposeFormat bool + Namespace string + StackFileContent string +} + +func (payload *kubernetesStackPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.StackFileContent) { + return portainer.Error("Invalid stack file content") + } + if govalidator.IsNull(payload.Namespace) { + return portainer.Error("Invalid namespace") + } + return nil +} + +type createKubernetesStackResponse struct { + Output string `json:"Output"` +} + +func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { + var payload kubernetesStackPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to deploy Kubernetes stack", err} + } + + resp := &createKubernetesStackResponse{ + Output: string(output), + } + + return response.JSON(w, resp) +} + +func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) { + handler.stackCreationMutex.Lock() + defer handler.stackCreationMutex.Unlock() + + return handler.KubernetesDeployer.Deploy(endpoint, data, composeFormat, namespace) +} diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index f0a6fc0bf..5271cffb1 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -22,6 +22,7 @@ type Handler struct { GitService portainer.GitService SwarmStackManager portainer.SwarmStackManager ComposeStackManager portainer.ComposeStackManager + KubernetesDeployer portainer.KubernetesDeployer } // NewHandler creates a handler to manage stack operations. diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index c57464c2c..b4a3a3992 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -66,6 +66,12 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID) case portainer.DockerComposeStack: return handler.createComposeStack(w, r, method, endpoint, tokenData.ID) + case portainer.KubernetesStack: + if tokenData.Role != portainer.AdministratorRole { + return &httperror.HandlerError{http.StatusForbidden, "Access denied", portainer.ErrUnauthorized} + } + + return handler.createKubernetesStack(w, r, endpoint) } return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)} diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index ce6a3840b..5df4f2e2b 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -73,7 +73,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } for _, endpoint := range endpoints { - if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && endpoint.Type == portainer.EdgeAgentEnvironment { + if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) { err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint relations in the database", err} diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index 2a498f82e..dee223853 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -64,7 +64,7 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque if params.endpoint.Type == portainer.AgentOnDockerEnvironment { return handler.proxyAgentWebsocketRequest(w, r, params) - } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + } else if params.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 8c3318a83..eb59956ff 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -70,7 +70,7 @@ func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request if params.endpoint.Type == portainer.AgentOnDockerEnvironment { return handler.proxyAgentWebsocketRequest(w, r, params) - } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + } else if params.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index eb86de0a9..05cd88cfc 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -4,18 +4,20 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes/cli" ) // Handler is the HTTP handler used to handle websocket operations. type Handler struct { *mux.Router - DataStore portainer.DataStore - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - requestBouncer *security.RequestBouncer - connectionUpgrader websocket.Upgrader + DataStore portainer.DataStore + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + KubernetesClientFactory *cli.ClientFactory + requestBouncer *security.RequestBouncer + connectionUpgrader websocket.Upgrader } // NewHandler creates a handler to manage websocket operations. @@ -29,5 +31,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec))) h.PathPrefix("/websocket/attach").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach))) + h.PathPrefix("/websocket/pod").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketPodExec))) return h } diff --git a/api/http/handler/websocket/hijack.go b/api/http/handler/websocket/hijack.go index f8a7b6624..a991f3bec 100644 --- a/api/http/handler/websocket/hijack.go +++ b/api/http/handler/websocket/hijack.go @@ -2,9 +2,10 @@ package websocket import ( "fmt" - "github.com/gorilla/websocket" "net/http" "net/http/httputil" + + "github.com/gorilla/websocket" ) func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error { @@ -24,8 +25,8 @@ func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, defer tcpConn.Close() errorChan := make(chan error, 1) - go streamFromTCPConnToWebsocketConn(websocketConn, brw, errorChan) - go streamFromWebsocketConnToTCPConn(websocketConn, tcpConn, errorChan) + go streamFromReaderToWebsocket(websocketConn, brw, errorChan) + go streamFromWebsocketToWriter(websocketConn, tcpConn, errorChan) err = <-errorChan if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go new file mode 100644 index 000000000..46f7f1dfd --- /dev/null +++ b/api/http/handler/websocket/pod.go @@ -0,0 +1,116 @@ +package websocket + +import ( + "io" + "log" + "net/http" + "strings" + + "github.com/gorilla/websocket" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + portainer "github.com/portainer/portainer/api" +) + +// websocketPodExec handles GET requests on /websocket/pod?token=&endpointId=&namespace=&podName=&containerName=&command= +// The request will be upgraded to the websocket protocol. +// Authentication and access is controlled via the mandatory token query parameter. +// The following parameters query parameters are mandatory: +// * token: JWT token used for authentication against this endpoint +// * endpointId: endpoint ID of the endpoint where the resource is located +// * namespace: namespace where the container is located +// * podName: name of the pod containing the container +// * containerName: name of the container +// * command: command to execute in the container +func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} + } + + namespace, err := request.RetrieveQueryParameter(r, "namespace", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: namespace", err} + } + + podName, err := request.RetrieveQueryParameter(r, "podName", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: podName", err} + } + + containerName, err := request.RetrieveQueryParameter(r, "containerName", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: containerName", err} + } + + command, err := request.RetrieveQueryParameter(r, "command", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: command", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + params := &webSocketRequestParams{ + endpoint: endpoint, + } + + r.Header.Del("Origin") + + if endpoint.Type == portainer.AgentOnKubernetesEnvironment { + err := handler.proxyAgentWebsocketRequest(w, r, params) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to agent", err} + } + return nil + } else if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + err := handler.proxyEdgeAgentWebsocketRequest(w, r, params) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to Edge agent", err} + } + return nil + } + + commandArray := strings.Split(command, " ") + + websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to upgrade the connection", err} + } + defer websocketConn.Close() + + stdinReader, stdinWriter := io.Pipe() + defer stdinWriter.Close() + stdoutReader, stdoutWriter := io.Pipe() + defer stdoutWriter.Close() + + errorChan := make(chan error, 1) + go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan) + go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan) + + cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} + } + + err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err} + } + + err = <-errorChan + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { + log.Printf("websocket error: %s \n", err.Error()) + } + + return nil +} diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go index bd8e3f4f7..9ec317d9b 100644 --- a/api/http/handler/websocket/proxy.go +++ b/api/http/handler/websocket/proxy.go @@ -33,7 +33,9 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r } func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { - agentURL, err := url.Parse(params.endpoint.URL) + // TODO: k8s merge - make sure this is still working with Docker agent + //agentURL, err := url.Parse(params.endpoint.URL) + agentURL, err := url.Parse(fmt.Sprintf("http://%s", params.endpoint.URL)) if err != nil { return err } diff --git a/api/http/handler/websocket/stream.go b/api/http/handler/websocket/stream.go index a598b7cb7..131951803 100644 --- a/api/http/handler/websocket/stream.go +++ b/api/http/handler/websocket/stream.go @@ -1,13 +1,15 @@ package websocket import ( - "bufio" - "github.com/gorilla/websocket" - "net" + "io" "unicode/utf8" + + "github.com/gorilla/websocket" ) -func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) { +const readerBufferSize = 2048 + +func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer, errorChan chan error) { for { _, in, err := websocketConn.ReadMessage() if err != nil { @@ -15,7 +17,7 @@ func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net break } - _, err = tcpConn.Write(in) + _, err = writer.Write(in) if err != nil { errorChan <- err break @@ -23,10 +25,10 @@ func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net } } -func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) { +func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) { for { - out := make([]byte, 2048) - _, err := br.Read(out) + out := make([]byte, readerBufferSize) + _, err := reader.Read(out) if err != nil { errorChan <- err break diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go index 149c488d9..513b5731d 100644 --- a/api/http/proxy/factory/docker.go +++ b/api/http/proxy/factory/docker.go @@ -32,7 +32,7 @@ func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) ( } func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port) } diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 4c12fb93b..09027d0c4 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -132,7 +132,7 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) { response, err := transport.HTTPTransport.RoundTrip(request) - if transport.endpoint.Type != portainer.EdgeAgentEnvironment { + if transport.endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return response, err } diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index 6ebedbb9f..e3e4c1e3b 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -6,7 +6,11 @@ import ( "net/http/httputil" "net/url" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/api/docker" ) @@ -21,20 +25,24 @@ var extensionPorts = map[portainer.ExtensionID]string{ type ( // ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions ProxyFactory struct { - dataStore portainer.DataStore - signatureService portainer.DigitalSignatureService - reverseTunnelService portainer.ReverseTunnelService - dockerClientFactory *docker.ClientFactory + dataStore portainer.DataStore + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + dockerClientFactory *docker.ClientFactory + kubernetesClientFactory *cli.ClientFactory + kubernetesTokenCacheManager *kubernetes.TokenCacheManager } ) // NewProxyFactory returns a pointer to a new instance of a ProxyFactory -func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *ProxyFactory { +func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *ProxyFactory { return &ProxyFactory{ - dataStore: dataStore, - signatureService: signatureService, - reverseTunnelService: tunnelService, - dockerClientFactory: clientFactory, + dataStore: dataStore, + signatureService: signatureService, + reverseTunnelService: tunnelService, + dockerClientFactory: clientFactory, + kubernetesClientFactory: kubernetesClientFactory, + kubernetesTokenCacheManager: kubernetesTokenCacheManager, } } @@ -74,6 +82,8 @@ func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (htt switch endpoint.Type { case portainer.AzureEnvironment: return newAzureProxy(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.KubernetesLocalEnvironment: + return factory.newKubernetesProxy(endpoint) } return factory.newDockerProxy(endpoint) diff --git a/api/http/proxy/factory/kubernetes.go b/api/http/proxy/factory/kubernetes.go new file mode 100644 index 000000000..2cb09dc62 --- /dev/null +++ b/api/http/proxy/factory/kubernetes.go @@ -0,0 +1,109 @@ +package factory + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +func (factory *ProxyFactory) newKubernetesProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + switch endpoint.Type { + case portainer.KubernetesLocalEnvironment: + return factory.newKubernetesLocalProxy(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment: + return factory.newKubernetesEdgeHTTPProxy(endpoint) + } + + return factory.newKubernetesAgentHTTPSProxy(endpoint) +} + +func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + remoteURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, true) + if err != nil { + return nil, err + } + + transport, err := kubernetes.NewLocalTransport(tokenManager) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = transport + + return proxy, nil +} + +func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port) + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, false) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint.ID, tokenManager) + + return proxy, nil +} + +func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + endpointURL := fmt.Sprintf("https://%s", endpoint.URL) + remoteURL, err := url.Parse(endpointURL) + if err != nil { + return nil, err + } + + remoteURL.Scheme = "https" + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, false) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager) + + return proxy, nil +} diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go new file mode 100644 index 000000000..0e84f2d83 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/token.go @@ -0,0 +1,79 @@ +package kubernetes + +import ( + "io/ioutil" + "sync" + + portainer "github.com/portainer/portainer/api" +) + +const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +type tokenManager struct { + tokenCache *tokenCache + kubecli portainer.KubeClient + dataStore portainer.DataStore + mutex sync.Mutex + adminToken string +} + +// NewTokenManager returns a pointer to a new instance of tokenManager. +// If the useLocalAdminToken parameter is set to true, it will search for the local admin service account +// and associate it to the manager. +func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore, cache *tokenCache, setLocalAdminToken bool) (*tokenManager, error) { + tokenManager := &tokenManager{ + tokenCache: cache, + kubecli: kubecli, + dataStore: dataStore, + mutex: sync.Mutex{}, + adminToken: "", + } + + if setLocalAdminToken { + token, err := ioutil.ReadFile(defaultServiceAccountTokenFile) + if err != nil { + return nil, err + } + + tokenManager.adminToken = string(token) + } + + return tokenManager, nil +} + +func (manager *tokenManager) getAdminServiceAccountToken() string { + return manager.adminToken +} + +func (manager *tokenManager) getUserServiceAccountToken(userID int, username string) (string, error) { + manager.mutex.Lock() + defer manager.mutex.Unlock() + + token, ok := manager.tokenCache.getToken(userID) + if !ok { + memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID)) + if err != nil { + return "", err + } + + teamIds := make([]int, 0) + for _, membership := range memberships { + teamIds = append(teamIds, int(membership.TeamID)) + } + + err = manager.kubecli.SetupUserServiceAccount(userID, username, teamIds) + if err != nil { + return "", err + } + + serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID, username) + if err != nil { + return "", err + } + + manager.tokenCache.addToken(userID, serviceAccountToken) + token = serviceAccountToken + } + + return token, nil +} diff --git a/api/http/proxy/factory/kubernetes/token_cache.go b/api/http/proxy/factory/kubernetes/token_cache.go new file mode 100644 index 000000000..552e6b3a1 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/token_cache.go @@ -0,0 +1,69 @@ +package kubernetes + +import ( + "strconv" + + "github.com/orcaman/concurrent-map" +) + +type ( + // TokenCacheManager represents a service used to manage multiple tokenCache objects. + TokenCacheManager struct { + tokenCaches cmap.ConcurrentMap + } + + tokenCache struct { + userTokenCache cmap.ConcurrentMap + } +) + +// NewTokenCacheManager returns a pointer to a new instance of TokenCacheManager +func NewTokenCacheManager() *TokenCacheManager { + return &TokenCacheManager{ + tokenCaches: cmap.New(), + } +} + +// CreateTokenCache will create a new tokenCache object, associate it to the manager map of caches +// and return a pointer to that tokenCache instance. +func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *tokenCache { + tokenCache := newTokenCache() + + key := strconv.Itoa(endpointID) + manager.tokenCaches.Set(key, tokenCache) + + return tokenCache +} + +// RemoveUserFromCache will ensure that the specific userID is removed from all registered caches. +func (manager *TokenCacheManager) RemoveUserFromCache(userID int) { + for cache := range manager.tokenCaches.IterBuffered() { + cache.Val.(*tokenCache).removeToken(userID) + } +} + +func newTokenCache() *tokenCache { + return &tokenCache{ + userTokenCache: cmap.New(), + } +} + +func (cache *tokenCache) getToken(userID int) (string, bool) { + key := strconv.Itoa(userID) + token, ok := cache.userTokenCache.Get(key) + if ok { + return token.(string), true + } + + return "", false +} + +func (cache *tokenCache) addToken(userID int, token string) { + key := strconv.Itoa(userID) + cache.userTokenCache.Set(key, token) +} + +func (cache *tokenCache) removeToken(userID int) { + key := strconv.Itoa(userID) + cache.userTokenCache.Remove(key) +} diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go new file mode 100644 index 000000000..7837ce647 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -0,0 +1,156 @@ +package kubernetes + +import ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/portainer/portainer/api/http/security" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +type ( + localTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + } + + agentTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + signatureService portainer.DigitalSignatureService + } + + edgeTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + reverseTunnelService portainer.ReverseTunnelService + endpointIdentifier portainer.EndpointID + } +) + +// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API +func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) { + config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true) + if err != nil { + return nil, err + } + + transport := &localTransport{ + httpTransport: &http.Transport{ + TLSClientConfig: config, + }, + tokenManager: tokenManager, + } + + return transport, nil +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent +func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport { + transport := &agentTransport{ + httpTransport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + tokenManager: tokenManager, + signatureService: signatureService, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent +func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport { + transport := &edgeTransport{ + httpTransport: &http.Transport{}, + tokenManager: tokenManager, + reverseTunnelService: reverseTunnelService, + endpointIdentifier: endpointIdentifier, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + response, err := transport.httpTransport.RoundTrip(request) + + if err == nil { + transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier) + } else { + transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier) + } + + return response, err +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 336481f9c..10c2f4cf6 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -4,8 +4,12 @@ import ( "net/http" "strconv" - "github.com/orcaman/concurrent-map" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + cmap "github.com/orcaman/concurrent-map" + "github.com/portainer/portainer/api/kubernetes/cli" + + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory" ) @@ -23,12 +27,12 @@ type ( ) // NewManager initializes a new proxy Service -func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *Manager { +func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager { return &Manager{ endpointProxies: cmap.New(), extensionProxies: cmap.New(), legacyExtensionProxies: cmap.New(), - proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory), + proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager), } } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 1fe96ad32..97abab731 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -125,7 +125,7 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp // AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge endpoint func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return errors.New("Invalid endpoint type") } diff --git a/api/http/server.go b/api/http/server.go index b86378512..4693c5334 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -1,23 +1,20 @@ package http import ( + "net/http" + "path/filepath" "time" - "github.com/portainer/portainer/api/http/handler/edgegroups" - "github.com/portainer/portainer/api/http/handler/edgestacks" - "github.com/portainer/portainer/api/http/handler/edgetemplates" - "github.com/portainer/portainer/api/http/handler/endpointedge" - "github.com/portainer/portainer/api/http/handler/support" - "github.com/portainer/portainer/api/internal/snapshot" - - "github.com/portainer/portainer/api/http/handler/roles" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler/auth" "github.com/portainer/portainer/api/http/handler/dockerhub" + "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" + "github.com/portainer/portainer/api/http/handler/edgestacks" + "github.com/portainer/portainer/api/http/handler/edgetemplates" + "github.com/portainer/portainer/api/http/handler/endpointedge" "github.com/portainer/portainer/api/http/handler/endpointgroups" "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" @@ -26,9 +23,11 @@ import ( "github.com/portainer/portainer/api/http/handler/motd" "github.com/portainer/portainer/api/http/handler/registries" "github.com/portainer/portainer/api/http/handler/resourcecontrols" + "github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/settings" "github.com/portainer/portainer/api/http/handler/stacks" "github.com/portainer/portainer/api/http/handler/status" + "github.com/portainer/portainer/api/http/handler/support" "github.com/portainer/portainer/api/http/handler/tags" "github.com/portainer/portainer/api/http/handler/teammemberships" "github.com/portainer/portainer/api/http/handler/teams" @@ -38,43 +37,43 @@ import ( "github.com/portainer/portainer/api/http/handler/webhooks" "github.com/portainer/portainer/api/http/handler/websocket" "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" - - "net/http" - "path/filepath" + "github.com/portainer/portainer/api/kubernetes/cli" ) // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - Status *portainer.Status - ReverseTunnelService portainer.ReverseTunnelService - ExtensionManager portainer.ExtensionManager - ComposeStackManager portainer.ComposeStackManager - CryptoService portainer.CryptoService - SignatureService portainer.DigitalSignatureService - SnapshotService *snapshot.Service - Snapshotter portainer.Snapshotter - FileService portainer.FileService - DataStore portainer.DataStore - GitService portainer.GitService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - SwarmStackManager portainer.SwarmStackManager - Handler *handler.Handler - SSL bool - SSLCert string - SSLKey string - DockerClientFactory *docker.ClientFactory + BindAddress string + AssetsPath string + Status *portainer.Status + ReverseTunnelService portainer.ReverseTunnelService + ExtensionManager portainer.ExtensionManager + ComposeStackManager portainer.ComposeStackManager + CryptoService portainer.CryptoService + SignatureService portainer.DigitalSignatureService + SnapshotService portainer.SnapshotService + FileService portainer.FileService + DataStore portainer.DataStore + GitService portainer.GitService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + SwarmStackManager portainer.SwarmStackManager + Handler *handler.Handler + SSL bool + SSLCert string + SSLKey string + DockerClientFactory *docker.ClientFactory + KubernetesClientFactory *cli.ClientFactory + KubernetesDeployer portainer.KubernetesDeployer } // Start starts the HTTP server func (server *Server) Start() error { - proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory) - authorizationService := authorization.NewService(server.DataStore) + kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager() + proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager) rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension) requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL) @@ -88,6 +87,7 @@ func (server *Server) Start() error { authHandler.LDAPService = server.LDAPService authHandler.ProxyManager = proxyManager authHandler.AuthorizationService = authorizationService + authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager var roleHandler = roles.NewHandler(requestBouncer) roleHandler.DataStore = server.DataStore @@ -116,8 +116,9 @@ func (server *Server) Start() error { endpointHandler.AuthorizationService = authorizationService endpointHandler.FileService = server.FileService endpointHandler.ProxyManager = proxyManager + endpointHandler.SnapshotService = server.SnapshotService + endpointHandler.ProxyManager = proxyManager endpointHandler.ReverseTunnelService = server.ReverseTunnelService - endpointHandler.Snapshotter = server.Snapshotter var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) endpointEdgeHandler.DataStore = server.DataStore @@ -163,6 +164,7 @@ func (server *Server) Start() error { stackHandler.FileService = server.FileService stackHandler.SwarmStackManager = server.SwarmStackManager stackHandler.ComposeStackManager = server.ComposeStackManager + stackHandler.KubernetesDeployer = server.KubernetesDeployer stackHandler.GitService = server.GitService var tagHandler = tags.NewHandler(requestBouncer) @@ -195,6 +197,7 @@ func (server *Server) Start() error { websocketHandler.DataStore = server.DataStore websocketHandler.SignatureService = server.SignatureService websocketHandler.ReverseTunnelService = server.ReverseTunnelService + websocketHandler.KubernetesClientFactory = server.KubernetesClientFactory var webhookHandler = webhooks.NewHandler(requestBouncer) webhookHandler.DataStore = server.DataStore diff --git a/api/internal/edge/edgegroup.go b/api/internal/edge/edgegroup.go index 968d5cff2..0b0140acb 100644 --- a/api/internal/edge/edgegroup.go +++ b/api/internal/edge/edgegroup.go @@ -13,7 +13,7 @@ func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []porta endpointIDs := []portainer.EndpointID{} for _, endpoint := range endpoints { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { continue } diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go index f5c0c49a5..da9565cce 100644 --- a/api/internal/snapshot/snapshot.go +++ b/api/internal/snapshot/snapshot.go @@ -7,16 +7,19 @@ import ( "github.com/portainer/portainer/api" ) -// Service repesents a service to manage system snapshots +// Service repesents a service to manage endpoint snapshots. +// It provides an interface to start background snapshots as well as +// specific Docker/Kubernetes endpoint snapshot methods. type Service struct { dataStore portainer.DataStore refreshSignal chan struct{} snapshotIntervalInSeconds float64 - snapshotter portainer.Snapshotter + dockerSnapshotter portainer.DockerSnapshotter + kubernetesSnapshotter portainer.KubernetesSnapshotter } // NewService creates a new instance of a service -func NewService(snapshotInterval string, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) (*Service, error) { +func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter) (*Service, error) { snapshotFrequency, err := time.ParseDuration(snapshotInterval) if err != nil { return nil, err @@ -25,11 +28,12 @@ func NewService(snapshotInterval string, dataStore portainer.DataStore, snapshot return &Service{ dataStore: dataStore, snapshotIntervalInSeconds: snapshotFrequency.Seconds(), - snapshotter: snapshotter, + dockerSnapshotter: dockerSnapshotter, + kubernetesSnapshotter: kubernetesSnapshotter, }, nil } -// Start starts the service +// Start will start a background routine to execute periodic snapshots of endpoints func (service *Service) Start() { if service.refreshSignal != nil { return @@ -62,6 +66,55 @@ func (service *Service) SetSnapshotInterval(snapshotInterval string) error { return nil } +// SupportDirectSnapshot checks whether an endpoint can be used to trigger a direct a snapshot. +// It is mostly true for all endpoints except Edge and Azure endpoints. +func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool { + switch endpoint.Type { + case portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment, portainer.AzureEnvironment: + return false + } + return true +} + +// SnapshotEndpoint will create a snapshot of the endpoint based on the endpoint type. +// If the snapshot is a success, it will be associated to the endpoint. +func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error { + switch endpoint.Type { + case portainer.AzureEnvironment: + return nil + case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment: + return service.snapshotKubernetesEndpoint(endpoint) + } + + return service.snapshotDockerEndpoint(endpoint) +} + +func (service *Service) snapshotKubernetesEndpoint(endpoint *portainer.Endpoint) error { + snapshot, err := service.kubernetesSnapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + if snapshot != nil { + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*snapshot} + } + + return nil +} + +func (service *Service) snapshotDockerEndpoint(endpoint *portainer.Endpoint) error { + snapshot, err := service.dockerSnapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + if snapshot != nil { + endpoint.Snapshots = []portainer.DockerSnapshot{*snapshot} + } + + return nil +} + func (service *Service) startSnapshotLoop() error { ticker := time.NewTicker(time.Duration(service.snapshotIntervalInSeconds) * time.Second) go func() { @@ -96,11 +149,11 @@ func (service *Service) snapshotEndpoints() error { } for _, endpoint := range endpoints { - if endpoint.Type == portainer.EdgeAgentEnvironment { + if !SupportDirectSnapshot(&endpoint) { continue } - snapshot, snapshotError := service.snapshotter.CreateSnapshot(&endpoint) + snapshotError := service.SnapshotEndpoint(&endpoint) latestEndpointReference, err := service.dataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -114,9 +167,8 @@ func (service *Service) snapshotEndpoints() error { latestEndpointReference.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/kubernetes.go b/api/kubernetes.go new file mode 100644 index 000000000..6ca8a3a78 --- /dev/null +++ b/api/kubernetes.go @@ -0,0 +1,11 @@ +package portainer + +func KubernetesDefault() KubernetesData { + return KubernetesData{ + Configuration: KubernetesConfiguration{ + UseLoadBalancer: false, + StorageClasses: []KubernetesStorageClassConfig{}, + }, + Snapshots: []KubernetesSnapshot{}, + } +} diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go new file mode 100644 index 000000000..bd79d3fce --- /dev/null +++ b/api/kubernetes/cli/access.go @@ -0,0 +1,86 @@ +package cli + +import ( + "encoding/json" + + portainer "github.com/portainer/portainer/api" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ( + accessPolicies struct { + UserAccessPolicies portainer.UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies portainer.TeamAccessPolicies `json:"TeamAccessPolicies"` + } + + namespaceAccessPolicies map[string]accessPolicies +) + +func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error { + configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + accessData := configMap.Data[portainerConfigMapAccessPoliciesKey] + + var accessPolicies namespaceAccessPolicies + err = json.Unmarshal([]byte(accessData), &accessPolicies) + if err != nil { + return err + } + + namespaces, err := kcl.cli.CoreV1().Namespaces().List(metav1.ListOptions{}) + if err != nil { + return err + } + + for _, namespace := range namespaces.Items { + if namespace.Name == defaultNamespace { + continue + } + + policies, ok := accessPolicies[namespace.Name] + if !ok { + err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil { + return err + } + continue + } + + if !hasUserAccessToNamespace(userID, teamIDs, policies) { + err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil { + return err + } + continue + } + + err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + } + + return nil +} + +func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies) bool { + _, userAccess := policies.UserAccessPolicies[portainer.UserID(userID)] + if userAccess { + return true + } + + for _, teamID := range teamIDs { + _, teamAccess := policies.TeamAccessPolicies[portainer.TeamID(teamID)] + if teamAccess { + return true + } + } + + return false +} diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go new file mode 100644 index 000000000..b87faac92 --- /dev/null +++ b/api/kubernetes/cli/client.go @@ -0,0 +1,145 @@ +package cli + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + cmap "github.com/orcaman/concurrent-map" + + portainer "github.com/portainer/portainer/api" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type ( + // ClientFactory is used to create Kubernetes clients + ClientFactory struct { + reverseTunnelService portainer.ReverseTunnelService + signatureService portainer.DigitalSignatureService + endpointClients cmap.ConcurrentMap + } + + // KubeClient represent a service used to execute Kubernetes operations + KubeClient struct { + cli *kubernetes.Clientset + } +) + +// NewClientFactory returns a new instance of a ClientFactory +func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory { + return &ClientFactory{ + signatureService: signatureService, + reverseTunnelService: reverseTunnelService, + endpointClients: cmap.New(), + } +} + +// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found. +// If no client is registered, it will create a new client, register it, and returns it. +func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { + key := strconv.Itoa(int(endpoint.ID)) + client, ok := factory.endpointClients.Get(key) + if !ok { + client, err := factory.createKubeClient(endpoint) + if err != nil { + return nil, err + } + + factory.endpointClients.Set(key, client) + return client, nil + } + + return client.(portainer.KubeClient), nil +} + +func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { + cli, err := factory.CreateClient(endpoint) + if err != nil { + return nil, err + } + + kubecli := &KubeClient{ + cli: cli, + } + + return kubecli, nil +} + +// CreateClient returns a pointer to a new Clientset instance +func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + switch endpoint.Type { + case portainer.KubernetesLocalEnvironment: + return buildLocalClient() + case portainer.AgentOnKubernetesEnvironment: + return factory.buildAgentClient(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment: + return factory.buildEdgeClient(endpoint) + } + + return nil, errors.New("unsupported endpoint type") +} + +type agentHeaderRoundTripper struct { + signatureHeader string + publicKeyHeader string + + roundTripper http.RoundTripper +} + +// RoundTrip is the implementation of the http.RoundTripper interface. +// It decorates the request with specific agent headers +func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add(portainer.PortainerAgentPublicKeyHeader, rt.publicKeyHeader) + req.Header.Add(portainer.PortainerAgentSignatureHeader, rt.signatureHeader) + + return rt.roundTripper.RoundTrip(req) +} + +func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL) + signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + config, err := clientcmd.BuildConfigFromFlags(endpointURL, "") + if err != nil { + return nil, err + } + config.Insecure = true + + config.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &agentHeaderRoundTripper{ + signatureHeader: signature, + publicKeyHeader: factory.signatureService.EncodedPublicKey(), + roundTripper: rt, + } + }) + + return kubernetes.NewForConfig(config) +} + +func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpointURL := fmt.Sprintf("http://localhost:%d/kubernetes", tunnel.Port) + + config, err := clientcmd.BuildConfigFromFlags(endpointURL, "") + if err != nil { + return nil, err + } + config.Insecure = true + + return kubernetes.NewForConfig(config) +} + +func buildLocalClient() (*kubernetes.Clientset, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + + return kubernetes.NewForConfig(config) +} diff --git a/api/kubernetes/cli/exec.go b/api/kubernetes/cli/exec.go new file mode 100644 index 000000000..1716b10e6 --- /dev/null +++ b/api/kubernetes/cli/exec.go @@ -0,0 +1,57 @@ +package cli + +import ( + "errors" + "io" + + "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + utilexec "k8s.io/client-go/util/exec" +) + +// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace +// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write +// to the stdout parameter. +// This function only works against a local endpoint using an in-cluster config. +func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error { + config, err := rest.InClusterConfig() + if err != nil { + return err + } + + req := kcl.cli.CoreV1().RESTClient(). + Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec") + + req.VersionedParams(&v1.PodExecOptions{ + Container: containerName, + Command: command, + Stdin: true, + Stdout: true, + Stderr: true, + TTY: true, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return err + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: stdout, + Tty: true, + }) + if err != nil { + if _, ok := err.(utilexec.ExitError); !ok { + return errors.New("unable to start exec process") + } + } + + return nil +} diff --git a/api/kubernetes/cli/naming.go b/api/kubernetes/cli/naming.go new file mode 100644 index 000000000..9c101e5bd --- /dev/null +++ b/api/kubernetes/cli/naming.go @@ -0,0 +1,26 @@ +package cli + +import "fmt" + +const ( + defaultNamespace = "default" + portainerNamespace = "portainer" + portainerUserCRName = "portainer-cr-user" + portainerUserCRBName = "portainer-crb-user" + portainerUserServiceAccountPrefix = "portainer-sa-user" + portainerRBPrefix = "portainer-rb" + portainerConfigMapName = "portainer-config" + portainerConfigMapAccessPoliciesKey = "NamespaceAccessPolicies" +) + +func userServiceAccountName(userID int, username string) string { + return fmt.Sprintf("%s-%d-%s", portainerUserServiceAccountPrefix, userID, username) +} + +func userServiceAccountTokenSecretName(serviceAccountName string) string { + return fmt.Sprintf("%s-secret", serviceAccountName) +} + +func namespaceClusterRoleBindingName(namespace string) string { + return fmt.Sprintf("%s-%s", portainerRBPrefix, namespace) +} diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go new file mode 100644 index 000000000..e19f1f22a --- /dev/null +++ b/api/kubernetes/cli/role.go @@ -0,0 +1,33 @@ +package cli + +import ( + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + { + Verbs: []string{"list"}, + Resources: []string{"namespaces", "nodes"}, + APIGroups: []string{""}, + }, + } +} + +func (kcl *KubeClient) createPortainerUserClusterRole() error { + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerUserCRName, + }, + Rules: getPortainerUserDefaultPolicies(), + } + + _, err := kcl.cli.RbacV1().ClusterRoles().Create(clusterRole) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} diff --git a/api/kubernetes/cli/secret.go b/api/kubernetes/cli/secret.go new file mode 100644 index 000000000..87ba35f53 --- /dev/null +++ b/api/kubernetes/cli/secret.go @@ -0,0 +1,74 @@ +package cli + +import ( + "errors" + "time" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error { + serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName) + + serviceAccountSecret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountSecretName, + Annotations: map[string]string{ + "kubernetes.io/service-account.name": serviceAccountName, + }, + }, + Type: "kubernetes.io/service-account-token", + } + + _, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Create(serviceAccountSecret) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (kcl *KubeClient) getServiceAccountToken(serviceAccountName string) (string, error) { + serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName) + + secret, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Get(serviceAccountSecretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + // API token secret is populated asynchronously. + // Is it created by the controller and will depend on the environment/secret-store: + // https://github.com/kubernetes/kubernetes/issues/67882#issuecomment-422026204 + // as a work-around, we wait for up to 5 seconds for the secret to be populated. + timeout := time.After(5 * time.Second) + searchingForSecret := true + for searchingForSecret { + select { + case <-timeout: + return "", errors.New("unable to find secret token associated to user service account (timeout)") + default: + secret, err = kcl.cli.CoreV1().Secrets(portainerNamespace).Get(serviceAccountSecretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + if len(secret.Data) > 0 { + searchingForSecret = false + break + } + + time.Sleep(1 * time.Second) + } + } + + secretTokenData, ok := secret.Data["token"] + if ok { + return string(secretTokenData), nil + } + + return "", errors.New("unable to find secret token associated to user service account") +} diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go new file mode 100644 index 000000000..1af1b47f9 --- /dev/null +++ b/api/kubernetes/cli/service_account.go @@ -0,0 +1,182 @@ +package cli + +import ( + "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user. +func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string) (string, error) { + serviceAccountName := userServiceAccountName(userID, username) + + return kcl.getServiceAccountToken(serviceAccountName) +} + +// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes +// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. +//It will also create required default RoleBinding and ClusterRoleBinding rules. +func (kcl *KubeClient) SetupUserServiceAccount(userID int, username string, teamIDs []int) error { + serviceAccountName := userServiceAccountName(userID, username) + + err := kcl.ensureRequiredResourcesExist() + if err != nil { + return err + } + + err = kcl.ensureServiceAccountForUserExists(serviceAccountName) + if err != nil { + return err + } + + return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName) +} + +func (kcl *KubeClient) ensureRequiredResourcesExist() error { + return kcl.createPortainerUserClusterRole() +} + +func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error { + err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName) + if err != nil { + return err + } + + err = kcl.createServiceAccountToken(serviceAccountName) + if err != nil { + return err + } + + err = kcl.ensureServiceAccountHasPortainerUserClusterRole(serviceAccountName) + if err != nil { + return err + } + + return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace) +} + +func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error { + serviceAccount := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + } + + _, err := kcl.cli.CoreV1().ServiceAccounts(namespace).Create(serviceAccount) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (kcl *KubeClient) ensureServiceAccountHasPortainerUserClusterRole(serviceAccountName string) error { + clusterRoleBinding, err := kcl.cli.RbacV1().ClusterRoleBindings().Get(portainerUserCRBName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + clusterRoleBinding = &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerUserCRBName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: portainerUserCRName, + }, + } + + _, err := kcl.cli.RbacV1().ClusterRoleBindings().Create(clusterRoleBinding) + return err + } else if err != nil { + return err + } + + for _, subject := range clusterRoleBinding.Subjects { + if subject.Name == serviceAccountName { + return nil + } + } + + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }) + + _, err = kcl.cli.RbacV1().ClusterRoleBindings().Update(clusterRoleBinding) + return err +} + +func (kcl *KubeClient) removeNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error { + roleBindingName := namespaceClusterRoleBindingName(namespace) + + roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + updatedSubjects := roleBinding.Subjects[:0] + + for _, subject := range roleBinding.Subjects { + if subject.Name != serviceAccountName { + updatedSubjects = append(updatedSubjects, subject) + } + } + + roleBinding.Subjects = updatedSubjects + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Update(roleBinding) + return err +} + +func (kcl *KubeClient) ensureNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error { + roleBindingName := namespaceClusterRoleBindingName(namespace) + + roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + roleBinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleBindingName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "edit", + }, + } + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Create(roleBinding) + return err + } else if err != nil { + return err + } + + for _, subject := range roleBinding.Subjects { + if subject.Name == serviceAccountName { + return nil + } + } + + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }) + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Update(roleBinding) + return err +} diff --git a/api/kubernetes/snapshot.go b/api/kubernetes/snapshot.go new file mode 100644 index 000000000..8382d95ab --- /dev/null +++ b/api/kubernetes/snapshot.go @@ -0,0 +1,83 @@ +package kubernetes + +import ( + "log" + "time" + + "github.com/portainer/portainer/api/kubernetes/cli" + + portainer "github.com/portainer/portainer/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Snapshotter struct { + clientFactory *cli.ClientFactory +} + +// NewSnapshotter returns a new Snapshotter instance +func NewSnapshotter(clientFactory *cli.ClientFactory) *Snapshotter { + return &Snapshotter{ + clientFactory: clientFactory, + } +} + +// CreateSnapshot creates a snapshot of a specific Kubernetes endpoint +func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.KubernetesSnapshot, error) { + client, err := snapshotter.clientFactory.CreateClient(endpoint) + if err != nil { + return nil, err + } + + return snapshot(client, endpoint) +} + +func snapshot(cli *kubernetes.Clientset, endpoint *portainer.Endpoint) (*portainer.KubernetesSnapshot, error) { + res := cli.RESTClient().Get().AbsPath("/healthz").Do() + if res.Error() != nil { + return nil, res.Error() + } + + snapshot := &portainer.KubernetesSnapshot{} + + err := snapshotVersion(snapshot, cli) + if err != nil { + log.Printf("[WARN] [kubernetes,snapshot] [message: unable to snapshot cluster version] [endpoint: %s] [err: %s]", endpoint.Name, err) + } + + err = snapshotNodes(snapshot, cli) + if err != nil { + log.Printf("[WARN] [kubernetes,snapshot] [message: unable to snapshot cluster nodes] [endpoint: %s] [err: %s]", endpoint.Name, err) + } + + snapshot.Time = time.Now().Unix() + return snapshot, nil +} + +func snapshotVersion(snapshot *portainer.KubernetesSnapshot, cli *kubernetes.Clientset) error { + versionInfo, err := cli.ServerVersion() + if err != nil { + return err + } + + snapshot.KubernetesVersion = versionInfo.GitVersion + return nil +} + +func snapshotNodes(snapshot *portainer.KubernetesSnapshot, cli *kubernetes.Clientset) error { + nodeList, err := cli.CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return err + } + + var totalCPUs, totalMemory int64 + for _, node := range nodeList.Items { + totalCPUs += node.Status.Capacity.Cpu().Value() + totalMemory += node.Status.Capacity.Memory().Value() + } + + snapshot.TotalCPU = totalCPUs + snapshot.TotalMemory = totalMemory + snapshot.NodeCount = len(nodeList.Items) + return nil +} diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index d3bf546c3..ec885b65b 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -37,7 +37,7 @@ func NewComposeStackManager(dataPath string, reverseTunnelService portainer.Reve func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) { endpointURL := endpoint.URL - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port) } diff --git a/api/portainer.go b/api/portainer.go index 05edb3866..582e7cf84 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1,6 +1,7 @@ package portainer import ( + "io" "time" ) @@ -60,42 +61,6 @@ type ( SnapshotInterval *string } - // CLIService represents a service for managing CLI - CLIService interface { - ParseFlags(version string) (*CLIFlags, error) - ValidateFlags(flags *CLIFlags) error - } - - // DataStore defines the interface to manage the data - DataStore interface { - Open() error - Init() error - Close() error - IsNew() bool - MigrateData() error - - DockerHub() DockerHubService - EdgeGroup() EdgeGroupService - EdgeJob() EdgeJobService - EdgeStack() EdgeStackService - Endpoint() EndpointService - EndpointGroup() EndpointGroupService - EndpointRelation() EndpointRelationService - Extension() ExtensionService - Registry() RegistryService - ResourceControl() ResourceControlService - Role() RoleService - Settings() SettingsService - Stack() StackService - Tag() TagService - TeamMembership() TeamMembershipService - Team() TeamService - TunnelServer() TunnelServerService - User() UserService - Version() VersionService - Webhook() WebhookService - } - // DockerHub represents all the required information to connect and use the // Docker Hub DockerHub struct { @@ -104,6 +69,34 @@ type ( Password string `json:"Password,omitempty"` } + // DockerSnapshot represents a snapshot of a specific Docker endpoint at a specific time + DockerSnapshot struct { + Time int64 `json:"Time"` + DockerVersion string `json:"DockerVersion"` + Swarm bool `json:"Swarm"` + TotalCPU int `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + RunningContainerCount int `json:"RunningContainerCount"` + StoppedContainerCount int `json:"StoppedContainerCount"` + HealthyContainerCount int `json:"HealthyContainerCount"` + UnhealthyContainerCount int `json:"UnhealthyContainerCount"` + VolumeCount int `json:"VolumeCount"` + ImageCount int `json:"ImageCount"` + ServiceCount int `json:"ServiceCount"` + StackCount int `json:"StackCount"` + SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"` + } + + // DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API + DockerSnapshotRaw struct { + Containers interface{} `json:"Containers"` + Volumes interface{} `json:"Volumes"` + Networks interface{} `json:"Networks"` + Images interface{} `json:"Images"` + Info interface{} `json:"Info"` + Version interface{} `json:"Version"` + } + // EdgeGroup represents an Edge group EdgeGroup struct { ID EdgeGroupID `json:"Id"` @@ -191,12 +184,13 @@ type ( AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` TagIDs []TagID `json:"TagIds"` Status EndpointStatus `json:"Status"` - Snapshots []Snapshot `json:"Snapshots"` + Snapshots []DockerSnapshot `json:"Snapshots"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` EdgeID string `json:"EdgeID,omitempty"` EdgeKey string `json:"EdgeKey"` EdgeCheckinInterval int `json:"EdgeCheckinInterval"` + Kubernetes KubernetesData `json:"Kubernetes"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -257,7 +251,6 @@ type ( EndpointStatus int // EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file - // // Deprecated EndpointSyncJob struct{} @@ -303,6 +296,33 @@ type ( // JobType represents a job type JobType int + // KubernetesData contains all the Kubernetes related endpoint information + KubernetesData struct { + Snapshots []KubernetesSnapshot `json:"Snapshots"` + Configuration KubernetesConfiguration `json:"Configuration"` + } + + // KubernetesSnapshot represents a snapshot of a specific Kubernetes endpoint at a specific time + KubernetesSnapshot struct { + Time int64 `json:"Time"` + KubernetesVersion string `json:"KubernetesVersion"` + NodeCount int `json:"NodeCount"` + TotalCPU int64 `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + } + + // KubernetesConfiguration represents the configuration of a Kubernetes endpoint + KubernetesConfiguration struct { + UseLoadBalancer bool `json:"UseLoadBalancer"` + StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` + } + + // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration + KubernetesStorageClassConfig struct { + Name string `json:"Name"` + AccessModes []string `json:"AccessModes"` + } + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` @@ -488,33 +508,8 @@ type ( DisplayExternalContributors bool } - // Snapshot represents a snapshot of a specific endpoint at a specific time - Snapshot struct { - Time int64 `json:"Time"` - DockerVersion string `json:"DockerVersion"` - Swarm bool `json:"Swarm"` - TotalCPU int `json:"TotalCPU"` - TotalMemory int64 `json:"TotalMemory"` - RunningContainerCount int `json:"RunningContainerCount"` - StoppedContainerCount int `json:"StoppedContainerCount"` - HealthyContainerCount int `json:"HealthyContainerCount"` - UnhealthyContainerCount int `json:"UnhealthyContainerCount"` - VolumeCount int `json:"VolumeCount"` - ImageCount int `json:"ImageCount"` - ServiceCount int `json:"ServiceCount"` - StackCount int `json:"StackCount"` - SnapshotRaw SnapshotRaw `json:"SnapshotRaw"` - } - - // SnapshotRaw represents all the information related to a snapshot as returned by the Docker API - SnapshotRaw struct { - Containers interface{} `json:"Containers"` - Volumes interface{} `json:"Volumes"` - Networks interface{} `json:"Networks"` - Images interface{} `json:"Images"` - Info interface{} `json:"Info"` - Version interface{} `json:"Version"` - } + // SnapshotJob represents a scheduled job that can create endpoint snapshots + SnapshotJob struct{} // Stack represents a Docker stack created via docker stack deploy Stack struct { @@ -733,6 +728,12 @@ type ( // WebhookType represents the type of resource a webhook is related to WebhookType int + // CLIService represents a service for managing CLI + CLIService interface { + ParseFlags(version string) (*CLIFlags, error) + ValidateFlags(flags *CLIFlags) error + } + // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { Up(stack *Stack, endpoint *Endpoint) error @@ -745,6 +746,36 @@ type ( CompareHashAndData(hash string, data string) error } + // DataStore defines the interface to manage the data + DataStore interface { + Open() error + Init() error + Close() error + IsNew() bool + MigrateData() error + + DockerHub() DockerHubService + EdgeGroup() EdgeGroupService + EdgeJob() EdgeJobService + EdgeStack() EdgeStackService + Endpoint() EndpointService + EndpointGroup() EndpointGroupService + EndpointRelation() EndpointRelationService + Extension() ExtensionService + Registry() RegistryService + ResourceControl() ResourceControlService + Role() RoleService + Settings() SettingsService + Stack() StackService + Tag() TagService + TeamMembership() TeamMembershipService + Team() TeamService + TunnelServer() TunnelServerService + User() UserService + Version() VersionService + Webhook() WebhookService + } + // DigitalSignatureService represents a service to manage digital signatures DigitalSignatureService interface { ParseKeyPair(private, public []byte) error @@ -760,6 +791,11 @@ type ( UpdateDockerHub(registry *DockerHub) error } + // DockerSnapshotter represents a service used to create Docker endpoint snapshots + DockerSnapshotter interface { + CreateSnapshot(endpoint *Endpoint) (*DockerSnapshot, error) + } + // EdgeGroupService represents a service to manage Edge groups EdgeGroupService interface { EdgeGroups() ([]EdgeGroup, error) @@ -876,6 +912,23 @@ type ( SetUserSessionDuration(userSessionDuration time.Duration) } + // KubeClient represents a service used to query a Kubernetes environment + KubeClient interface { + SetupUserServiceAccount(userID int, username string, teamIDs []int) error + GetServiceAccountBearerToken(userID int, username string) (string, error) + StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error + } + + // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint + KubernetesDeployer interface { + Deploy(endpoint *Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) + } + + // KubernetesSnapshotter represents a service used to create Kubernetes endpoint snapshots + KubernetesSnapshotter interface { + CreateSnapshot(endpoint *Endpoint) (*KubernetesSnapshot, error) + } + // LDAPService represents a service used to authenticate users against a LDAP/AD LDAPService interface { AuthenticateUser(username, password string, settings *LDAPSettings) error @@ -904,7 +957,7 @@ type ( // ReverseTunnelService represensts a service used to manage reverse tunnel connections. ReverseTunnelService interface { - StartTunnelServer(addr, port string, snapshotter Snapshotter) error + StartTunnelServer(addr, port string, snapshotService SnapshotService) error GenerateEdgeKey(url, host string, endpointIdentifier int) string SetTunnelStatusToActive(endpointID EndpointID) SetTunnelStatusToRequired(endpointID EndpointID) error @@ -933,11 +986,6 @@ type ( Start() error } - // Snapshotter represents a service used to create endpoint snapshots - Snapshotter interface { - CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) - } - // StackService represents a service for managing stack data StackService interface { Stack(ID StackID) (*Stack, error) @@ -949,6 +997,13 @@ type ( GetNextIdentifier() int } + // StackService represents a service for managing endpoint snapshots + SnapshotService interface { + Start() + SetSnapshotInterval(snapshotInterval string) error + SnapshotEndpoint(endpoint *Endpoint) error + } + // SwarmStackManager represents a service to manage Swarm stacks SwarmStackManager interface { Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) @@ -1048,6 +1103,8 @@ const ( PortainerAgentSignatureHeader = "X-PortainerAgent-Signature" // PortainerAgentPublicKeyHeader represent the name of the header containing the public key PortainerAgentPublicKeyHeader = "X-PortainerAgent-PublicKey" + // PortainerAgentKubernetesSATokenHeader represent the name of the header containing a Kubernetes SA token + PortainerAgentKubernetesSATokenHeader = "X-PortainerAgent-SA-Token" // PortainerAgentSignatureMessage represents the message used to create a digital signature // to be used when communicating with an agent PortainerAgentSignatureMessage = "Portainer-App" @@ -1115,8 +1172,14 @@ const ( AgentOnDockerEnvironment // AzureEnvironment represents an endpoint connected to an Azure environment AzureEnvironment - // EdgeAgentEnvironment represents an endpoint connected to an Edge agent - EdgeAgentEnvironment + // EdgeAgentOnDockerEnvironment represents an endpoint connected to an Edge agent deployed on a Docker environment + EdgeAgentOnDockerEnvironment + // KubernetesLocalEnvironment represents an endpoint connected to a local Kubernetes environment + KubernetesLocalEnvironment + // AgentOnKubernetesEnvironment represents an endpoint connected to a Portainer agent deployed on a Kubernetes environment + AgentOnKubernetesEnvironment + // EdgeAgentOnKubernetesEnvironment represents an endpoint connected to an Edge agent deployed on a Kubernetes environment + EdgeAgentOnKubernetesEnvironment ) const ( @@ -1185,6 +1248,8 @@ const ( DockerSwarmStack // DockerComposeStack represents a stack managed via docker-compose DockerComposeStack + // KubernetesStack represents a stack managed via kubectl + KubernetesStack ) const ( diff --git a/app/__module.js b/app/__module.js index 0aed3bfa1..886251dfe 100644 --- a/app/__module.js +++ b/app/__module.js @@ -30,6 +30,7 @@ angular.module('portainer', [ 'portainer.agent', 'portainer.azure', 'portainer.docker', + 'portainer.kubernetes', 'portainer.edge', 'portainer.extensions', 'portainer.integrations', diff --git a/app/assets/css/app.css b/app/assets/css/app.css index f57e3802f..b521dd890 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -118,7 +118,6 @@ a[ng-click] { .fa.tooltip-icon { margin-left: 5px; font-size: 1.3em; - color: #337ab7; } .fa.green-icon { @@ -151,6 +150,11 @@ a[ng-click] { margin-right: 5px; } +.widget .widget-body table tbody .label-margins { + margin-left: 5px; + margin-right: 0; +} + .widget .widget-body table tbody .fit-text-size { font-size: 90% !important; } @@ -888,6 +892,17 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { vertical-align: middle; } +.striked::after { + border-bottom: 0.2em solid #777777; + content: ''; + left: 0; + margin-top: calc(0.2em / 2 * -1); + position: absolute; + right: 0; + top: 50%; + z-index: 2; +} + /*bootbox override*/ .modal-open { padding-right: 0 !important; @@ -966,3 +981,40 @@ json-tree .branch-preview { opacity: 0.5; } /* !json-tree override */ + +/* uib-progressbar override */ +.progress-bar { + color: #4e4e4e; +} +/* !uib-progressbar override */ + +.loading-view-area { + height: 85%; + display: flex; + align-items: center; +} + +/* bootstrap extend */ +.input-xs { + height: 22px; + padding: 2px 5px; + font-size: 12px; + line-height: 1.5; /* If Placeholder of the input is moved up, rem/modify this. */ + border-radius: 3px; +} +/* !bootstrap extend */ + +/* spinkit override */ +.sk-fold { + width: 57px; + height: 57px; +} + +.sk-fold-cube { + background-color: white; +} + +.sk-fold-cube:before { + background-color: #337ab7; +} +/* !spinkit override */ diff --git a/app/assets/images/kubernetes_endpoint.png b/app/assets/images/kubernetes_endpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..3a85817cc4b3b749446560712627f3dfab6df825 GIT binary patch literal 4079 zcmV$y00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru;|T%=3m=he!TJCI4}3{P zK~!ko)th;IR8^M8zvtDfC6zrPB)Fh}LZc!y!=mES3W@^_4GPljN3l`R)(@i4qN4UV zC<=`PSF6ACs;XL3h2^kPs<%?gcBbbgX9B2*G<+TjECF&)mYY1ZxPpU)K@2l=uT6t) zo$M^UFNeHL8$c0okBEHoPY<{W&g_^f@q0=-1uBy~EP! z-@^%T2zWt67B(IGp9ydpa4Rqu$lSEg&%#e?*mf|0+nLyFyaC0}yE(XjL zzX$#%BK4;OxSR%UrUG{Z^GyqO91ifnqH;W5g-vVUq$~@v(=_Hz%ArpeJGxB(rU9>r zi1$>qE~f}~Nm#4S_czq>;wLrKdevzqhO`uosiQL(b-5eB2fPd1DI%&ppe0$LN~w;( z0$?m)e|4pY55KOXu+qeClQTjz0YlL#N9X22ZtfYD4&Vpg10EKU!eoF;658wuOa~@Z z*I9i0O+7Dt>LCzN*pdt;=jZDL4LDY2pe3<~bAY#itG#~3>z{jgb)^TVy?xqrJB4-6bwU%kd}<*9qkYMS-++lj)0lK)I;k!J z_#famKw)di(GG6xPfxz-bry-hcA6 z5S(R&l9*rgb}?^a4uBWv91nC$5EcMwZF1nqXSs^(*Rp)@d12(?9`h zzVk71W-*pU3NR<0hFgMh*H=4z>^dAkPBZbUuqg&jPFwaA0V8m{%w)>}Kfo373KA`j zIiCaA-&#)j_ID$Z!j{T$PHYz#mkcu0lzGjeC3mJ)!xcN*ut~ zS{7g4_(U<~9+Jk7z}*YW35MgeB9vdRnOy)txyNGAw8DlM-0VFsPtN7)OI#;_ z&LU#9Mp}JWZt>EROZftd2VbiIAS+elvuE>RU|tL}1+xA2&Wx%=9rC4DOS{hCdassF{kuD3#>`FExNUH1 z!tvN@k4Dl~EedX~S+`Qkl){2|1DPMU`d@m^XgkfyaKG88J^0V!~8( z#T7JQ=51O0b$VV58SOh3jC`IrG#xX+1Vz_`Rof$2E@)6{v@XX2Bm?-yektftggk0y z2@5`|iMeOYm8q*(-*`+ZH z-C9#HU?_B(3;{lj_F*09wQ-LRr#->8i>VmU!xH*GHN@aw2Cp2J z>@m5s$7?ksw8KR~=6@LL)0_AE>HokjC1my3tTW;WheXEAE*=@KcFFZib@|fUcCv0?t~tvA)>^Ta%xPjTj&s=XYubAf7yTR}N3VQ;n{{ zgQGLzPtq1@5De>=!mxfRRMlBAiYToy6Yj)l0=td`=zD?VA^=fJ4OEIv!=EgsxYBI3 znAdGWs!J%yw(<1s*<5sRS>h@sp7r$ z_4GK~&Y+8(T-MXcg=g7uIRuwI7R-fb*?Ir*#<*VVRm@mg!N$G*xMOcU5cz$A4h*jM zt4Pl&9^j=@JH!O~Z}<5rtv0#p5?3TuT)o}Pj5jK=35_8nY<6F4^|lH4aFKy-gF{6@ z4i*Oa%j#M*4eU1I#rtyTf1xu%eD80lqf3FE>w3Gw`%l7@Q5k%>qXd^j;>2Uvb2P|B zo$cdvV94HMK}?JIHL$?M=gSy2AcZYI_}R42PgSi20@wFVVg960E2e%wxti=$j()D>~Z1182sadHlH_aNHe%m2Gx=mtQG;6l|sQ0T#vj6ay zEFOHdJW>|}WTXgvx;bJ()1(*5@dgy@pDUnaR>*Wl40Q9%yVa4FPJ^4wy(>Gy?Bz8U z+YbgJ&BK79SijrH+MV9W+*)$C?2skeG++ZlFMRtFDuPTQ8Y&Q7a!t%)Cbjq>Or<;@E1KccrutUr{rZ28Y zxZ4p;xHE3aqThuMfcr({h(v9Tz7O0yZE*$bcKVVA-K~?Ix2AW9X}N}b!vK7~)yve^ zDq;$b26=T#F8#tR?DZ>Bq8b1SxaFBr4iyHI+V(N6kV}NEW^Uw#_zT^&l~yXkhR9!JB7$XiOOk#`J99edzJfdBqq zZ&F*M)5m0z;nsNla}O^q_aI?&7EQ>{wvm?+N_5IS7DbgNhN*Dqf?+ADYAvRY&fvF0 z(pdP3C*iYRp_Z|WTdz$8SRf*=CA3}I1pEfmV$jgXi>RzkY`4B3+s5*l`Dg+|W+eVx z^uEwnb3;cW#ywlg!NSCv65Y?X^UkAr0Q*FwXG@1#v{t|Z+-TFNT=rysi+O5O|9SD= zTr`2b$ATO$GuxFYlvRfoY1VJE6Y^u-CM>!?7oZyWc|6Uw30Oo3v)g5q8yLO4UqYposX|YV0L22{jB@@$R}h=KNQ6 zn|_s_ty5TTwEG~8QjnXWQQ@)L^xK)YXEAc18{iHRd9$s9X<7nc5s{U^YrnoGmD{ds zdw-|6(m174KO(dV*0f+qA6Em|KO_P+*?t&GsnwQZ@YrWcI9z0$_7gtI$i?S5c=MqS z02_fpt@&-c0EbD;1^lEG9j|?)s97)i? this.itemCanExpand(item)).length; - }; + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + if (!item.Expanded) { + this.state.expandAll = false; + } + }; - this.expandAll = function () { - this.state.expandAll = !this.state.expandAll; - _.forEach(this.dataset, (item) => { - if (this.itemCanExpand(item)) { - this.expandItem(item, this.state.expandAll); - } - }); - }; + this.itemCanExpand = function (item) { + return item.GlobalIPv6Address !== ''; + }; - this.$onInit = function () { - this.setDefaults(); - this.prepareTableFromDataset(); + this.hasExpandableItems = function () { + return _.filter(this.dataset, (item) => this.itemCanExpand(item)).length; + }; - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.dataset, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); } + }); + }; - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } + this.$onInit = function () { + this.setDefaults(); + this.prepareTableFromDataset(); - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } - _.forEach(this.dataset, (item) => { - item.Expanded = true; - item.Highlighted = true; - }); - }; - } - ]); \ No newline at end of file + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + + _.forEach(this.dataset, (item) => { + item.Expanded = true; + item.Highlighted = true; + }); + }; + }, +]); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 819fc8c0b..72e7e4a11 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -59,6 +59,9 @@ +
+ Close +
diff --git a/app/docker/components/volumesNFSForm/volumesnfsForm.html b/app/docker/components/volumesNFSForm/volumesnfsForm.html index 3a197ebcd..fa6496f30 100644 --- a/app/docker/components/volumesNFSForm/volumesnfsForm.html +++ b/app/docker/components/volumesNFSForm/volumesnfsForm.html @@ -38,7 +38,14 @@
- +
diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 65a90ba72..c7a92f17a 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -1,4 +1,4 @@ -import { ContainerDetailsViewModel, ContainerViewModel, ContainerStatsViewModel } from '../models/container'; +import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container'; angular.module('portainer.docker').factory('ContainerService', [ '$q', diff --git a/app/docker/views/containers/inspect/containerinspect.html b/app/docker/views/containers/inspect/containerinspect.html index def4954d5..eeedd2360 100644 --- a/app/docker/views/containers/inspect/containerinspect.html +++ b/app/docker/views/containers/inspect/containerinspect.html @@ -9,7 +9,7 @@
- + diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.html b/app/edge/components/edge-groups-selector/edge-groups-selector.html index 2badca9a2..41fa18a68 100644 --- a/app/edge/components/edge-groups-selector/edge-groups-selector.html +++ b/app/edge/components/edge-groups-selector/edge-groups-selector.html @@ -1,16 +1,10 @@ - + {{ $item.Name }} - + {{ item.Name }} diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.js b/app/edge/components/edge-groups-selector/edge-groups-selector.js index 3322780b8..66f8c429d 100644 --- a/app/edge/components/edge-groups-selector/edge-groups-selector.js +++ b/app/edge/components/edge-groups-selector/edge-groups-selector.js @@ -2,6 +2,6 @@ angular.module('portainer.edge').component('edgeGroupsSelector', { templateUrl: './edge-groups-selector.html', bindings: { model: '=', - items: '<' - } + items: '<', + }, }); diff --git a/app/edge/components/group-form/groupForm.html b/app/edge/components/group-form/groupForm.html index a33a534df..b527440d0 100644 --- a/app/edge/components/group-form/groupForm.html +++ b/app/edge/components/group-form/groupForm.html @@ -59,11 +59,9 @@ tags="$ctrl.tags" groups="$ctrl.groups" has-backend-pagination="true" - - on-associate="$ctrl.associateEndpoint" - on-dissociate="$ctrl.dissociateEndpoint" + on-associate="($ctrl.associateEndpoint)" + on-dissociate="($ctrl.dissociateEndpoint)" > -
diff --git a/app/extensions/registry-management/models/registryRepository.js b/app/extensions/registry-management/models/registryRepository.js index 4e772ae21..eb723bd75 100644 --- a/app/extensions/registry-management/models/registryRepository.js +++ b/app/extensions/registry-management/models/registryRepository.js @@ -1,4 +1,5 @@ import _ from 'lodash-es'; + export function RegistryRepositoryViewModel(item) { if (item.name && item.tags) { this.Name = item.name; diff --git a/app/extensions/registry-management/services/registryV2Service.js b/app/extensions/registry-management/services/registryV2Service.js index e877a4b99..242ca364b 100644 --- a/app/extensions/registry-management/services/registryV2Service.js +++ b/app/extensions/registry-management/services/registryV2Service.js @@ -1,6 +1,5 @@ import _ from 'lodash-es'; -import { RepositoryShortTag } from '../models/repositoryTag'; -import { RepositoryAddTagPayload } from '../models/repositoryTag'; +import { RepositoryAddTagPayload, RepositoryShortTag } from '../models/repositoryTag'; import { RegistryRepositoryViewModel } from '../models/registryRepository'; import genericAsyncGenerator from './genericAsyncGenerator'; diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js index 6bf5cb475..e8ab74f09 100644 --- a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js +++ b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag'; +import { RepositoryShortTag, RepositoryTagViewModel } from '../../../models/repositoryTag'; angular.module('portainer.app').controller('RegistryRepositoryController', [ '$q', diff --git a/app/index.html b/app/index.html index ef7137d15..89e9c87ea 100644 --- a/app/index.html +++ b/app/index.html @@ -26,7 +26,7 @@ id="page-wrapper" ng-class="{ open: toggle && ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint'].indexOf($state.current.name) === -1, - nopadding: ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint'].indexOf($state.current.name) > -1 || applicationState.loading + nopadding: ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint', 'portainer.logout'].indexOf($state.current.name) > -1 || applicationState.loading }" ng-cloak > diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js new file mode 100644 index 000000000..29fd6e81e --- /dev/null +++ b/app/kubernetes/__module.js @@ -0,0 +1,261 @@ +angular.module('portainer.kubernetes', ['portainer.app']).config([ + '$stateRegistryProvider', + function ($stateRegistryProvider) { + 'use strict'; + + const kubernetes = { + name: 'kubernetes', + url: '/kubernetes', + parent: 'root', + abstract: true, + resolve: { + endpointID: [ + 'EndpointProvider', + '$state', + function (EndpointProvider, $state) { + const id = EndpointProvider.endpointID(); + if (!id) { + return $state.go('portainer.home'); + } + }, + ], + }, + }; + + const applications = { + name: 'kubernetes.applications', + url: '/applications', + views: { + 'content@': { + component: 'kubernetesApplicationsView', + }, + }, + }; + + const applicationCreation = { + name: 'kubernetes.applications.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateApplicationView', + }, + }, + }; + + const application = { + name: 'kubernetes.applications.application', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesApplicationView', + }, + }, + }; + + const applicationEdit = { + name: 'kubernetes.applications.application.edit', + url: '/edit', + views: { + 'content@': { + component: 'kubernetesCreateApplicationView', + }, + }, + }; + + const applicationConsole = { + name: 'kubernetes.applications.application.console', + url: '/:pod/console', + views: { + 'content@': { + component: 'kubernetesApplicationConsoleView', + }, + }, + }; + + const applicationLogs = { + name: 'kubernetes.applications.application.logs', + url: '/:pod/logs', + views: { + 'content@': { + component: 'kubernetesApplicationLogsView', + }, + }, + }; + + const stacks = { + name: 'kubernetes.stacks', + url: '/stacks', + abstract: true, + }; + + const stack = { + name: 'kubernetes.stacks.stack', + url: '/:namespace/:name', + abstract: true, + }; + + const stackLogs = { + name: 'kubernetes.stacks.stack.logs', + url: '/logs', + views: { + 'content@': { + component: 'kubernetesStackLogsView', + }, + }, + }; + + const configurations = { + name: 'kubernetes.configurations', + url: '/configurations', + views: { + 'content@': { + component: 'kubernetesConfigurationsView', + }, + }, + }; + + const configurationCreation = { + name: 'kubernetes.configurations.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateConfigurationView', + }, + }, + }; + + const configuration = { + name: 'kubernetes.configurations.configuration', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesConfigurationView', + }, + }, + }; + + const cluster = { + name: 'kubernetes.cluster', + url: '/cluster', + views: { + 'content@': { + component: 'kubernetesClusterView', + }, + }, + }; + + const node = { + name: 'kubernetes.cluster.node', + url: '/:name', + views: { + 'content@': { + component: 'kubernetesNodeView', + }, + }, + }; + + const dashboard = { + name: 'kubernetes.dashboard', + url: '/dashboard', + views: { + 'content@': { + component: 'kubernetesDashboardView', + }, + }, + }; + + const deploy = { + name: 'kubernetes.deploy', + url: '/deploy', + views: { + 'content@': { + component: 'kubernetesDeployView', + }, + }, + }; + + const resourcePools = { + name: 'kubernetes.resourcePools', + url: '/pools', + views: { + 'content@': { + component: 'kubernetesResourcePoolsView', + }, + }, + }; + + const resourcePoolCreation = { + name: 'kubernetes.resourcePools.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateResourcePoolView', + }, + }, + }; + + const resourcePool = { + name: 'kubernetes.resourcePools.resourcePool', + url: '/:id', + views: { + 'content@': { + component: 'kubernetesResourcePoolView', + }, + }, + }; + + const resourcePoolAccess = { + name: 'kubernetes.resourcePools.resourcePool.access', + url: '/access', + views: { + 'content@': { + component: 'kubernetesResourcePoolAccessView', + }, + }, + }; + + const volumes = { + name: 'kubernetes.volumes', + url: '/volumes', + views: { + 'content@': { + component: 'kubernetesVolumesView', + }, + }, + }; + + const volume = { + name: 'kubernetes.volumes.volume', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesVolumeView', + }, + }, + }; + + $stateRegistryProvider.register(kubernetes); + $stateRegistryProvider.register(applications); + $stateRegistryProvider.register(applicationCreation); + $stateRegistryProvider.register(application); + $stateRegistryProvider.register(applicationEdit); + $stateRegistryProvider.register(applicationConsole); + $stateRegistryProvider.register(applicationLogs); + $stateRegistryProvider.register(stacks); + $stateRegistryProvider.register(stack); + $stateRegistryProvider.register(stackLogs); + $stateRegistryProvider.register(configurations); + $stateRegistryProvider.register(configurationCreation); + $stateRegistryProvider.register(configuration); + $stateRegistryProvider.register(cluster); + $stateRegistryProvider.register(dashboard); + $stateRegistryProvider.register(deploy); + $stateRegistryProvider.register(node); + $stateRegistryProvider.register(resourcePools); + $stateRegistryProvider.register(resourcePoolCreation); + $stateRegistryProvider.register(resourcePool); + $stateRegistryProvider.register(resourcePoolAccess); + $stateRegistryProvider.register(volumes); + $stateRegistryProvider.register(volume); + }, +]); diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html new file mode 100644 index 000000000..0b5ba805a --- /dev/null +++ b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html @@ -0,0 +1,157 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Image + + + + + + Status + + + + + + Node + + + + + + Creation date + + + + Actions
{{ item.Name }}{{ image }}
{{ item.Status }} + + + {{ item.Node }} + + + - + {{ item.CreationDate | getisodate }} + Logs + + Console + +
Loading...
No pod available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js new file mode 100644 index 000000000..faa7df25f --- /dev/null +++ b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js @@ -0,0 +1,12 @@ +angular.module('portainer.kubernetes').component('kubernetesPodsDatatable', { + templateUrl: './podsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html new file mode 100644 index 000000000..4dee337e4 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -0,0 +1,193 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Stack + + + + + + Resource pool + + + + + + Image + + + + + Deployment + + Publishing mode + + + Created + + + +
+ + + + + {{ item.Name }} + system + external + {{ item.StackName }} + {{ item.ResourcePool }} + {{ item.Image }} + Replicated + Global + {{ item.RunningPodsCount }} / {{ item.TotalPodsCount }} + + + + + {{ item.ServiceType | kubernetesApplicationServiceTypeText }} + + + + - + {{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js new file mode 100644 index 000000000..fea41d460 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js @@ -0,0 +1,15 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatable', { + templateUrl: './applicationsDatatable.html', + controller: 'KubernetesApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + onPublishingModeClick: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js new file mode 100644 index 000000000..aaee33363 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js @@ -0,0 +1,77 @@ +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + /** + * Do not allow applications in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemNamespace(item); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html new file mode 100644 index 000000000..cf66330d6 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html @@ -0,0 +1,192 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Application + + + + + Publishing mode + + + Exposed port + + + + + + Container port + + + + + + Protocol + + + +
+ + + + + {{ item.Name }} + system + external + + + Load balancer + + {{ item.LoadBalancerIPAddress }} + pending + + + Internal + Cluster + + {{ item.Ports[0].Port }} + + access + + {{ item.Ports[0].TargetPort }}{{ item.Ports[0].Protocol }}
-- + {{ port.Port }} + + access + + {{ port.TargetPort }}{{ port.Protocol }}
Loading...
No application port mapping available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js new file mode 100644 index 000000000..139b000c5 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsPortsDatatable', { + templateUrl: './applicationsPortsDatatable.html', + controller: 'KubernetesApplicationsPortsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js new file mode 100644 index 000000000..e7086a40c --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js @@ -0,0 +1,103 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return item.Ports.length > 1; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html new file mode 100644 index 000000000..4bef79088 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html @@ -0,0 +1,183 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + Stack + + + + + + Resource pool + + + + + + Applications + + + + + Actions +
+ + + + + + + + + {{ item.Name }} + + {{ item.ResourcePool }} + system + {{ item.Applications.length }} + Logs +
+ {{ app.Name }} + external +
Loading...
No stack available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js new file mode 100644 index 000000000..3826ba62b --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDatatable', { + templateUrl: './applicationsStacksDatatable.html', + controller: 'KubernetesApplicationsStacksDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + removeAction: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js new file mode 100644 index 000000000..cc77ba205 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js @@ -0,0 +1,110 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsRepeaterChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + /** + * Do not allow applications in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemNamespace(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return item.Applications.length > 0; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html new file mode 100644 index 000000000..daedca169 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html @@ -0,0 +1,162 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Resource Pool + + + + + + Type + + + + + + Created + + + +
+ + + + + {{ item.Name }} + external + unused + system + + {{ item.Namespace }} + {{ item.Type | kubernetesConfigurationTypeText }}{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}
Loading...
No configuration available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js new file mode 100644 index 000000000..bf68244e0 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationsDatatable', { + templateUrl: './configurationsDatatable.html', + controller: 'KubernetesConfigurationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + removeAction: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js new file mode 100644 index 000000000..18417d951 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js @@ -0,0 +1,83 @@ +import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; + +angular.module('portainer.docker').controller('KubernetesConfigurationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + const ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace); + }; + + this.isSystemToken = function (item) { + return KubernetesConfigurationHelper.isSystemToken(item); + }; + + this.isSystemConfig = function (item) { + return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item); + }; + + this.isExternalConfiguration = function (item) { + return KubernetesConfigurationHelper.isExternalConfiguration(item); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemConfig(item) || (ctrl.settings.showSystem && ctrl.isAdmin); + }; + + /** + * Do not allow configurations in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemConfig(item) && !item.Used; + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html new file mode 100644 index 000000000..6a6d14603 --- /dev/null +++ b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html @@ -0,0 +1,134 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Date + + + + + + Kind + + + + + + Type + + + + + + Message + + + +
{{ item.Date | getisodate }}{{ item.Involved.kind }}{{ item.Type }}{{ item.Message }}
Loading...
No event available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js new file mode 100644 index 000000000..ef4f16504 --- /dev/null +++ b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesEventsDatatable', { + templateUrl: './eventsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + loading: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html new file mode 100644 index 000000000..12a0d200b --- /dev/null +++ b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html @@ -0,0 +1,127 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Image + + + +
{{ item.Name }}{{ item.StackName }}{{ item.Image }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js new file mode 100644 index 000000000..35729102f --- /dev/null +++ b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesIntegratedApplicationsDatatable', { + templateUrl: './integratedApplicationsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html new file mode 100644 index 000000000..4ab919bd1 --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html @@ -0,0 +1,156 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Resource pool + + + + + + Image + + + + + + CPU reservation + + + + + + Memory reservation + + + +
+ {{ item.Name }} + system + external + {{ item.StackName }} + {{ item.ResourcePool }} + {{ item.Image }}{{ item.CPU | kubernetesApplicationCPUValue }}{{ item.Memory | humansize }}
Loading...
No stack available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js new file mode 100644 index 000000000..ed9879453 --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesNodeApplicationsDatatable', { + templateUrl: './nodeApplicationsDatatable.html', + controller: 'KubernetesNodeApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js new file mode 100644 index 000000000..9bb25c50a --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js @@ -0,0 +1,54 @@ +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.$onInit = function () { + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html new file mode 100644 index 000000000..7d90d2379 --- /dev/null +++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html @@ -0,0 +1,164 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Role + + + + + + Status + + + + + + CPU + + + + + + Memory + + + + + + Version + + + + + + IP Address + + + +
+ + {{ item.Name }} + + + {{ item.Name }} + {{ item.Role }}{{ item.Status }}{{ item.CPU }}{{ item.Memory | humansize }}{{ item.Version }}{{ item.IPAddress }}
Loading...
No node available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js new file mode 100644 index 000000000..3c1312302 --- /dev/null +++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', { + templateUrl: './nodesDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + isAdmin: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html new file mode 100644 index 000000000..c08cd865b --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html @@ -0,0 +1,145 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Image + + + + + + CPU reservation + + + + + + Memory reservation + + + +
+ {{ item.Name }} + external + {{ item.StackName }}{{ item.Image }}{{ item.CPU | kubernetesApplicationCPUValue }}{{ item.Memory | humansize }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js new file mode 100644 index 000000000..02dba5e79 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolApplicationsDatatable', { + templateUrl: './resourcePoolApplicationsDatatable.html', + controller: 'KubernetesResourcePoolApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js new file mode 100644 index 000000000..e97cf734d --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js @@ -0,0 +1,47 @@ +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesResourcePoolApplicationsDatatableController', [ + '$scope', + '$controller', + 'DatatableService', + function ($scope, $controller, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.$onInit = function () { + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html new file mode 100644 index 000000000..088f737df --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html @@ -0,0 +1,160 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Quota + + + + + + Created + + + + + Actions +
+ + + + + {{ item.Namespace.Name }} + system + {{ item.Quota ? 'Yes' : 'No' }} {{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }} + + Manage access + + - +
Loading...
No resource pool available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js new file mode 100644 index 000000000..8023b8518 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', { + templateUrl: './resourcePoolsDatatable.html', + controller: 'KubernetesResourcePoolsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js new file mode 100644 index 000000000..b3181bfe9 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js @@ -0,0 +1,77 @@ +angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [ + '$scope', + '$controller', + 'Authentication', + 'KubernetesNamespaceHelper', + 'DatatableService', + function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.canManageAccess = function (item) { + return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); + }; + + this.disableRemove = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default'; + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin); + }; + + /** + * Do not allow system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.disableRemove(item); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html new file mode 100644 index 000000000..7955c6ed2 --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html @@ -0,0 +1,192 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Resource pool + + + + + + Used by + + + + + + Storage + + + + + + Size + + + + + + Created + + + +
+ + + + + {{ + item.PersistentVolumeClaim.Name + }} + system + external + unused + + {{ item.ResourcePool.Namespace.Name }} + + {{ item.Applications[0].Name }} + - + + {{ item.PersistentVolumeClaim.StorageClass.Name }} + + {{ item.PersistentVolumeClaim.Storage }} + + {{ item.PersistentVolumeClaim.CreationDate | getisodate }} + {{ item.PersistentVolumeClaim.ApplicationOwner ? 'by ' + item.PersistentVolumeClaim.ApplicationOwner : '' }} +
Loading...
No volume available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js new file mode 100644 index 000000000..ddbdacf7f --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumesDatatable', { + templateUrl: './volumesDatatable.html', + controller: 'KubernetesVolumesDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js new file mode 100644 index 000000000..e9dc9e478 --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js @@ -0,0 +1,91 @@ +import angular from 'angular'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; + +// TODO: review - refactor to use `extends GenericDatatableController` +class KubernetesVolumesDatatableController { + /* @ngInject */ + constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + this.$async = $async; + this.$controller = $controller; + this.Authentication = Authentication; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.DatatableService = DatatableService; + + this.onInit = this.onInit.bind(this); + this.allowSelection = this.allowSelection.bind(this); + this.isDisplayed = this.isDisplayed.bind(this); + } + + onSettingsShowSystemChange() { + this.DatatableService.setDataTableSettings(this.tableKey, this.settings); + } + + disableRemove(item) { + return this.isSystemNamespace(item) || this.isUsed(item); + } + + isUsed(item) { + return KubernetesVolumeHelper.isUsed(item); + } + + isSystemNamespace(item) { + return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name); + } + + isDisplayed(item) { + return !this.isSystemNamespace(item) || this.showSystem; + } + + isExternalVolume(item) { + return KubernetesVolumeHelper.isExternalVolume(item); + } + + allowSelection(item) { + return !this.disableRemove(item); + } + + async onInit() { + this.setDefaults(); + this.prepareTableFromDataset(); + this.isAdmin = this.Authentication.isAdmin(); + + this.state.orderBy = this.orderBy; + var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = this.DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = this.DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + + this.settings.showSystem = false; + } + + $onInit() { + const ctrl = angular.extend({}, this.$controller('GenericDatatableController'), this); + angular.extend(this, ctrl); + return this.$async(this.onInit); + } +} + +export default KubernetesVolumesDatatableController; +angular.module('portainer.kubernetes').controller('KubernetesVolumesDatatableController', KubernetesVolumesDatatableController); diff --git a/app/kubernetes/components/feedback-panel/feedbackPanel.html b/app/kubernetes/components/feedback-panel/feedbackPanel.html new file mode 100644 index 000000000..115a175ea --- /dev/null +++ b/app/kubernetes/components/feedback-panel/feedbackPanel.html @@ -0,0 +1,9 @@ + + +

+ + Kubernetes support in Portainer is now in RC stage. Contribute and share your feedback in + our official repository. +

+
+
diff --git a/app/kubernetes/components/feedback-panel/feedbackPanel.js b/app/kubernetes/components/feedback-panel/feedbackPanel.js new file mode 100644 index 000000000..9bfe07254 --- /dev/null +++ b/app/kubernetes/components/feedback-panel/feedbackPanel.js @@ -0,0 +1,3 @@ +angular.module('portainer.kubernetes').component('kubernetesFeedbackPanel', { + templateUrl: './feedbackPanel.html', +}); diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html new file mode 100644 index 000000000..a7db2fc8f --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html @@ -0,0 +1,97 @@ + +
+ Data +
+ +
+ +
+ + Switch to advanced mode to copy and paste multiple key/values +
+
+ + Generate a configuration entry per line, use YAML format +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+ +

This field is required.

+
+

This key is already defined.

+
+
+ +
+ +
+ +
+
+ +

This field is required.

+
+
+
+ +
+
+
+ +
+
+
+ +
+ + +
+
diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js new file mode 100644 index 000000000..c5f717839 --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationData', { + templateUrl: './kubernetesConfigurationData.html', + controller: 'KubernetesConfigurationDataController', + bindings: { + formValues: '=', + isValid: '=', + }, +}); diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js new file mode 100644 index 000000000..1dcc43ce8 --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js @@ -0,0 +1,68 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import { KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues'; +import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; + +class KubernetesConfigurationDataController { + /* @ngInject */ + constructor($async) { + this.$async = $async; + + this.editorUpdate = this.editorUpdate.bind(this); + this.editorUpdateAsync = this.editorUpdateAsync.bind(this); + this.onFileLoad = this.onFileLoad.bind(this); + this.onFileLoadAsync = this.onFileLoadAsync.bind(this); + } + + onChangeKey() { + this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key)); + this.isValid = Object.keys(this.state.duplicateKeys).length === 0; + } + + addEntry() { + this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry()); + } + + removeEntry(index) { + this.formValues.Data.splice(index, 1); + this.onChangeKey(); + } + + async editorUpdateAsync(cm) { + this.formValues.DataYaml = cm.getValue(); + } + + editorUpdate(cm) { + return this.$async(this.editorUpdateAsync, cm); + } + + async onFileLoadAsync(event) { + const entry = new KubernetesConfigurationFormValuesDataEntry(); + entry.Key = event.target.fileName; + entry.Value = event.target.result; + this.formValues.Data.push(entry); + this.onChangeKey(); + } + + onFileLoad(event) { + return this.$async(this.onFileLoadAsync, event); + } + + addEntryFromFile(file) { + if (file) { + const temporaryFileReader = new FileReader(); + temporaryFileReader.fileName = file.name; + temporaryFileReader.onload = this.onFileLoad; + temporaryFileReader.readAsText(file); + } + } + + $onInit() { + this.state = { + duplicateKeys: {}, + }; + } +} + +export default KubernetesConfigurationDataController; +angular.module('portainer.kubernetes').controller('KubernetesConfigurationDataController', KubernetesConfigurationDataController); diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html new file mode 100644 index 000000000..354ba3760 --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js new file mode 100644 index 000000000..c12568fa7 --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js @@ -0,0 +1,6 @@ +angular.module('portainer.kubernetes').component('kubernetesSidebarContent', { + templateUrl: './kubernetesSidebarContent.html', + bindings: { + adminAccess: '<', + }, +}); diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.html b/app/kubernetes/components/resource-reservation/resourceReservation.html new file mode 100644 index 000000000..e03e62ace --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservation.html @@ -0,0 +1,32 @@ +
+
+ Resource reservation +
+
+ +

+ {{ $ctrl.description }} +

+
+
+
+ +
+ + {{ $ctrl.memory }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryUsage }}% + +
+
+
+ +
+ + {{ $ctrl.cpu | kubernetesApplicationCPUValue }} / {{ $ctrl.cpuLimit }} - {{ $ctrl.cpuUsage }}% + +
+
+
diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.js b/app/kubernetes/components/resource-reservation/resourceReservation.js new file mode 100644 index 000000000..617f70079 --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservation.js @@ -0,0 +1,11 @@ +angular.module('portainer.kubernetes').component('kubernetesResourceReservation', { + templateUrl: './resourceReservation.html', + controller: 'KubernetesResourceReservationController', + bindings: { + description: '@', + cpu: '<', + cpuLimit: '<', + memory: '<', + memoryLimit: '<', + }, +}); diff --git a/app/kubernetes/components/resource-reservation/resourceReservationController.js b/app/kubernetes/components/resource-reservation/resourceReservationController.js new file mode 100644 index 000000000..f55a014a1 --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservationController.js @@ -0,0 +1,23 @@ +import angular from 'angular'; + +class KubernetesResourceReservationController { + usageValues() { + if (this.cpuLimit) { + this.cpuUsage = Math.round((this.cpu / this.cpuLimit) * 100); + } + if (this.memoryLimit) { + this.memoryUsage = Math.round((this.memory / this.memoryLimit) * 100); + } + } + + $onInit() { + this.usageValues(); + } + + $onChanges() { + this.usageValues(); + } +} + +export default KubernetesResourceReservationController; +angular.module('portainer.kubernetes').controller('KubernetesResourceReservationController', KubernetesResourceReservationController); diff --git a/app/kubernetes/components/view-header/viewHeader.html b/app/kubernetes/components/view-header/viewHeader.html new file mode 100644 index 000000000..79ede2708 --- /dev/null +++ b/app/kubernetes/components/view-header/viewHeader.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/kubernetes/components/view-header/viewHeader.js b/app/kubernetes/components/view-header/viewHeader.js new file mode 100644 index 000000000..f1fdbbb6b --- /dev/null +++ b/app/kubernetes/components/view-header/viewHeader.js @@ -0,0 +1,9 @@ +angular.module('portainer.kubernetes').component('kubernetesViewHeader', { + templateUrl: './viewHeader.html', + transclude: true, + bindings: { + viewReady: '<', + title: '@', + state: '@', + }, +}); diff --git a/app/kubernetes/components/view-loading/viewLoading.html b/app/kubernetes/components/view-loading/viewLoading.html new file mode 100644 index 000000000..edf23b89e --- /dev/null +++ b/app/kubernetes/components/view-loading/viewLoading.html @@ -0,0 +1,8 @@ +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/components/view-loading/viewLoading.js b/app/kubernetes/components/view-loading/viewLoading.js new file mode 100644 index 000000000..0c4724b04 --- /dev/null +++ b/app/kubernetes/components/view-loading/viewLoading.js @@ -0,0 +1,6 @@ +angular.module('portainer.kubernetes').component('kubernetesViewLoading', { + templateUrl: './viewLoading.html', + bindings: { + viewReady: '<', + }, +}); diff --git a/app/kubernetes/components/yaml-inspector/yamlInspector.html b/app/kubernetes/components/yaml-inspector/yamlInspector.html new file mode 100644 index 000000000..1f2627e88 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspector.html @@ -0,0 +1,7 @@ +
+ +
+ Copy to clipboard + +
+
diff --git a/app/kubernetes/components/yaml-inspector/yamlInspector.js b/app/kubernetes/components/yaml-inspector/yamlInspector.js new file mode 100644 index 000000000..c84bd8828 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspector.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesYamlInspector', { + templateUrl: './yamlInspector.html', + controller: 'KubernetesYamlInspectorController', + bindings: { + key: '@', + data: '<', + }, +}); diff --git a/app/kubernetes/components/yaml-inspector/yamlInspectorController.js b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js new file mode 100644 index 000000000..82b009316 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js @@ -0,0 +1,17 @@ +import angular from 'angular'; + +class KubernetesYamlInspectorController { + /* @ngInject */ + + constructor(clipboard) { + this.clipboard = clipboard; + } + + copyYAML() { + this.clipboard.copyText(this.data); + $('#copyNotificationYAML').show().fadeOut(2500); + } +} + +export default KubernetesYamlInspectorController; +angular.module('portainer.kubernetes').controller('KubernetesYamlInspectorController', KubernetesYamlInspectorController); diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js new file mode 100644 index 000000000..5c7cf7476 --- /dev/null +++ b/app/kubernetes/converters/application.js @@ -0,0 +1,302 @@ +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; + +import { + KubernetesApplication, + KubernetesApplicationConfigurationVolume, + KubernetesApplicationDataAccessPolicies, + KubernetesApplicationDeploymentTypes, + KubernetesApplicationPersistedFolder, + KubernetesApplicationPublishingTypes, + KubernetesApplicationTypes, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationNote, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationStackNameLabel, +} from 'Kubernetes/models/application/models'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment'; +import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet'; +import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; +import KubernetesServiceConverter from 'Kubernetes/converters/service'; +import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; +import PortainerError from 'Portainer/error'; + +class KubernetesApplicationConverter { + static applicationCommon(res, data, service) { + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; + res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : ''; + res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : ''; + res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name; + res.ResourcePool = data.metadata.namespace; + res.Image = data.spec.template.spec.containers[0].image; + res.CreationDate = data.metadata.creationTimestamp; + res.Pods = data.Pods; + res.Env = data.spec.template.spec.containers[0].env; + const limits = { + Cpu: 0, + Memory: 0, + }; + res.Limits = _.reduce( + data.spec.template.spec.containers, + (acc, item) => { + if (item.resources.limits && item.resources.limits.cpu) { + acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.limits.cpu); + } + if (item.resources.limits && item.resources.limits.memory) { + acc.Memory += filesizeParser(item.resources.limits.memory, { base: 10 }); + } + return acc; + }, + limits + ); + + const requests = { + Cpu: 0, + Memory: 0, + }; + res.Requests = _.reduce( + data.spec.template.spec.containers, + (acc, item) => { + if (item.resources.requests && item.resources.requests.cpu) { + acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.requests.cpu); + } + if (item.resources.requests && item.resources.requests.memory) { + acc.Memory += filesizeParser(item.resources.requests.memory, { base: 10 }); + } + return acc; + }, + requests + ); + + if (service) { + const serviceType = service.spec.type; + res.ServiceType = serviceType; + res.ServiceId = service.metadata.uid; + res.ServiceName = service.metadata.name; + + if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { + if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) { + res.LoadBalancerIPAddress = service.status.loadBalancer.ingress[0].ip || service.status.loadBalancer.ingress[0].hostname; + } + } + + const ports = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); + res.PublishedPorts = service.spec.ports; + _.forEach(res.PublishedPorts, (publishedPort) => { + if (isNaN(publishedPort.targetPort)) { + const targetPort = _.find(ports, { name: publishedPort.targetPort }); + if (targetPort) { + publishedPort.targetPort = targetPort.containerPort; + } + } + }); + } + + res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : []; + + // TODO: review + // this if() fixs direct use of PVC reference inside spec.template.spec.containers[0].volumeMounts + // instead of referencing the PVC the "good way" using spec.template.spec.volumes array + // Basically it creates an "in-memory" reference for the PVC, as if it was saved in + // spec.template.spec.volumes and retrieved from here. + // + // FIX FOR SFS ONLY ; as far as we know it's not possible to do this with DEPLOYMENTS/DAEMONSETS + // + // This may lead to destructing behaviours when we will allow external apps to be edited. + // E.G. if we try to generate the formValues and patch the app, SFS reference will be created under + // spec.template.spec.volumes and not be referenced directly inside spec.template.spec.containers[0].volumeMounts + // As we preserve original SFS name and try to build around it, it SHOULD be fine, but we definitely need to test this + // before allowing external apps modification + if (data.spec.volumeClaimTemplates) { + const vcTemplates = _.map(data.spec.volumeClaimTemplates, (vc) => { + return { + name: vc.metadata.name, + persistentVolumeClaim: { claimName: vc.metadata.name }, + }; + }); + const inexistingPVC = _.filter(vcTemplates, (vc) => { + return !_.find(res.Volumes, { persistentVolumeClaim: { claimName: vc.persistentVolumeClaim.claimName } }); + }); + res.Volumes = _.concat(res.Volumes, inexistingPVC); + } + + const persistedFolders = _.filter(res.Volumes, (volume) => volume.persistentVolumeClaim || volume.hostPath); + + res.PersistedFolders = _.map(persistedFolders, (volume) => { + const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + + if (matchingVolumeMount) { + const persistedFolder = new KubernetesApplicationPersistedFolder(); + persistedFolder.MountPath = matchingVolumeMount.mountPath; + + if (volume.persistentVolumeClaim) { + persistedFolder.PersistentVolumeClaimName = volume.persistentVolumeClaim.claimName; + } else { + persistedFolder.HostPath = volume.hostPath.path; + } + + return persistedFolder; + } + }); + + res.PersistedFolders = _.without(res.PersistedFolders, undefined); + + res.ConfigurationVolumes = _.reduce( + data.spec.template.spec.volumes, + (acc, volume) => { + if (volume.configMap || volume.secret) { + const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + + if (matchingVolumeMount) { + let items = []; + let configurationName = ''; + + if (volume.configMap) { + items = volume.configMap.items; + configurationName = volume.configMap.name; + } else { + items = volume.secret.items; + configurationName = volume.secret.secretName; + } + + if (!items) { + const configurationVolume = new KubernetesApplicationConfigurationVolume(); + configurationVolume.fileMountPath = matchingVolumeMount.mountPath; + configurationVolume.rootMountPath = matchingVolumeMount.mountPath; + configurationVolume.configurationName = configurationName; + + acc.push(configurationVolume); + } else { + _.forEach(items, (item) => { + const configurationVolume = new KubernetesApplicationConfigurationVolume(); + configurationVolume.fileMountPath = matchingVolumeMount.mountPath + '/' + item.path; + configurationVolume.rootMountPath = matchingVolumeMount.mountPath; + configurationVolume.configurationKey = item.key; + configurationVolume.configurationName = configurationName; + + acc.push(configurationVolume); + }); + } + } + } + + return acc; + }, + [] + ); + } + + static apiDeploymentToApplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT; + res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; + res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0; + res.TotalPodsCount = data.spec.replicas; + return res; + } + + static apiDaemonSetToApplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.DAEMONSET; + res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; + res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0; + res.TotalPodsCount = data.status.desiredNumberScheduled; + return res; + } + + static apiStatefulSetToapplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.STATEFULSET; + res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; + res.RunningPodsCount = data.status.readyReplicas || 0; + res.TotalPodsCount = data.spec.replicas; + res.HeadlessServiceName = data.spec.serviceName; + return res; + } + + static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims) { + const res = new KubernetesApplicationFormValues(); + res.ApplicationType = app.ApplicationType; + res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); + res.Name = app.Name; + res.StackName = app.StackName; + res.ApplicationOwner = app.ApplicationOwner; + res.Image = app.Image; + res.ReplicaCount = app.TotalPodsCount; + res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory); + res.CpuLimit = app.Limits.Cpu; + res.DeploymentType = app.DeploymentType; + res.DataAccessPolicy = app.DataAccessPolicy; + res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env); + res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders + res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); + + if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { + res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) { + res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER; + } else { + res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; + } + res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); + return res; + } + + static applicationFormValuesToApplication(formValues) { + const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues); + const rwx = _.find(claims, (item) => _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined; + + const deployment = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED && + (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED))) || + formValues.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT; + + const statefulSet = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED && + claims.length > 0 && + formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.ISOLATED) || + formValues.ApplicationType === KubernetesApplicationTypes.STATEFULSET; + + const daemonSet = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.GLOBAL && + (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED && rwx))) || + formValues.ApplicationType === KubernetesApplicationTypes.DAEMONSET; + + let app; + if (deployment) { + app = KubernetesDeploymentConverter.applicationFormValuesToDeployment(formValues, claims); + } else if (statefulSet) { + app = KubernetesStatefulSetConverter.applicationFormValuesToStatefulSet(formValues, claims); + } else if (daemonSet) { + app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims); + } else { + throw new PortainerError('Unable to determine which association to use'); + } + + let headlessService; + if (statefulSet) { + headlessService = KubernetesServiceConverter.applicationFormValuesToHeadlessService(formValues); + } + + let service = KubernetesServiceConverter.applicationFormValuesToService(formValues); + if (!service.Ports.length) { + service = undefined; + } + return [app, headlessService, service, claims]; + } +} + +export default KubernetesApplicationConverter; diff --git a/app/kubernetes/converters/configMap.js b/app/kubernetes/converters/configMap.js new file mode 100644 index 000000000..36025c506 --- /dev/null +++ b/app/kubernetes/converters/configMap.js @@ -0,0 +1,81 @@ +import _ from 'lodash-es'; +import YAML from 'yaml'; +import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models'; +import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads'; +import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigMapConverter { + /** + * API ConfigMap to front ConfigMap + */ + static apiToConfigMap(data, yaml) { + const res = new KubernetesConfigMap(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : ''; + res.CreationDate = data.metadata.creationTimestamp; + res.Yaml = yaml ? yaml.data : ''; + res.Data = data.data; + return res; + } + + /** + * Generate a default ConfigMap Model + * with ID = 0 (showing it's a default) + * but setting his Namespace and Name + */ + static defaultConfigMap(namespace, name) { + const res = new KubernetesConfigMap(); + res.Name = name; + res.Namespace = namespace; + return res; + } + + /** + * CREATE payload + */ + static createPayload(data) { + const res = new KubernetesConfigMapCreatePayload(); + res.metadata.name = data.Name; + res.metadata.namespace = data.Namespace; + res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner; + res.data = data.Data; + return res; + } + + /** + * UPDATE payload + */ + static updatePayload(data) { + const res = new KubernetesConfigMapUpdatePayload(); + res.metadata.uid = data.Id; + res.metadata.name = data.Name; + res.metadata.namespace = data.Namespace; + res.data = data.Data; + return res; + } + + static configurationFormValuesToConfigMap(formValues) { + const res = new KubernetesConfigMap(); + res.Id = formValues.Id; + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.ConfigurationOwner = formValues.ConfigurationOwner; + if (formValues.IsSimple) { + res.Data = _.reduce( + formValues.Data, + (acc, entry) => { + acc[entry.Key] = entry.Value; + return acc; + }, + {} + ); + } else { + res.Data = YAML.parse(formValues.DataYaml); + } + return res; + } +} + +export default KubernetesConfigMapConverter; diff --git a/app/kubernetes/converters/configuration.js b/app/kubernetes/converters/configuration.js new file mode 100644 index 000000000..232fe5b79 --- /dev/null +++ b/app/kubernetes/converters/configuration.js @@ -0,0 +1,31 @@ +import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigurationConverter { + static secretToConfiguration(secret) { + const res = new KubernetesConfiguration(); + res.Type = KubernetesConfigurationTypes.SECRET; + res.Id = secret.Id; + res.Name = secret.Name; + res.Namespace = secret.Namespace; + res.CreationDate = secret.CreationDate; + res.Yaml = secret.Yaml; + res.Data = secret.Data; + res.ConfigurationOwner = secret.ConfigurationOwner; + return res; + } + + static configMapToConfiguration(configMap) { + const res = new KubernetesConfiguration(); + res.Type = KubernetesConfigurationTypes.CONFIGMAP; + res.Id = configMap.Id; + res.Name = configMap.Name; + res.Namespace = configMap.Namespace; + res.CreationDate = configMap.CreationDate; + res.Yaml = configMap.Yaml; + res.Data = configMap.Data; + res.ConfigurationOwner = configMap.ConfigurationOwner; + return res; + } +} + +export default KubernetesConfigurationConverter; diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js new file mode 100644 index 000000000..fa74b6bf1 --- /dev/null +++ b/app/kubernetes/converters/daemonSet.js @@ -0,0 +1,79 @@ +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; +import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationNote, + KubernetesPortainerApplicationOwnerLabel, +} from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; + +class KubernetesDaemonSetConverter { + /** + * Generate KubernetesDaemonSet from KubenetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToDaemonSet(formValues, volumeClaims) { + const res = new KubernetesDaemonSet(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from DaemonSet + * @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from + */ + static createPayload(daemonSet) { + const payload = new KubernetesDaemonSetCreatePayload(); + payload.metadata.name = daemonSet.Name; + payload.metadata.namespace = daemonSet.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note; + payload.spec.replicas = daemonSet.ReplicaCount; + payload.spec.selector.matchLabels.app = daemonSet.Name; + payload.spec.template.metadata.labels.app = daemonSet.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; + payload.spec.template.spec.containers[0].name = daemonSet.Name; + payload.spec.template.spec.containers[0].image = daemonSet.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes); + if (daemonSet.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = daemonSet.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = daemonSet.MemoryLimit; + } + if (daemonSet.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = daemonSet.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = daemonSet.CpuLimit; + } + if (!daemonSet.CpuLimit && !daemonSet.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldDaemonSet, newDaemonSet) { + const oldPayload = KubernetesDaemonSetConverter.createPayload(oldDaemonSet); + const newPayload = KubernetesDaemonSetConverter.createPayload(newDaemonSet); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesDaemonSetConverter; diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js new file mode 100644 index 000000000..a38126d5e --- /dev/null +++ b/app/kubernetes/converters/deployment.js @@ -0,0 +1,80 @@ +import * as JsonPatch from 'fast-json-patch'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesDeploymentCreatePayload } from 'Kubernetes/models/deployment/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationNote, +} from 'Kubernetes/models/application/models'; + +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; + +class KubernetesDeploymentConverter { + /** + * Generate KubernetesDeployment from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToDeployment(formValues, volumeClaims) { + const res = new KubernetesDeployment(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.ReplicaCount = formValues.ReplicaCount; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from Deployment + * @param {KubernetesDeploymentPayload} model Deployment to genereate payload from + */ + static createPayload(deployment) { + const payload = new KubernetesDeploymentCreatePayload(); + payload.metadata.name = deployment.Name; + payload.metadata.namespace = deployment.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note; + payload.spec.replicas = deployment.ReplicaCount; + payload.spec.selector.matchLabels.app = deployment.Name; + payload.spec.template.metadata.labels.app = deployment.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; + payload.spec.template.spec.containers[0].name = deployment.Name; + payload.spec.template.spec.containers[0].image = deployment.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes); + if (deployment.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = deployment.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = deployment.MemoryLimit; + } + if (deployment.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = deployment.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = deployment.CpuLimit; + } + if (!deployment.CpuLimit && !deployment.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldDeployment, newDeployment) { + const oldPayload = KubernetesDeploymentConverter.createPayload(oldDeployment); + const newPayload = KubernetesDeploymentConverter.createPayload(newDeployment); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesDeploymentConverter; diff --git a/app/kubernetes/converters/event.js b/app/kubernetes/converters/event.js new file mode 100644 index 000000000..5dd3c9d2f --- /dev/null +++ b/app/kubernetes/converters/event.js @@ -0,0 +1,15 @@ +import { KubernetesEvent } from 'Kubernetes/models/event/models'; + +class KubernetesEventConverter { + static apiToEvent(data) { + const res = new KubernetesEvent(); + res.Id = data.metadata.uid; + res.Date = data.lastTimestamp || data.eventTime; + res.Type = data.type; + res.Message = data.message; + res.Involved = data.involvedObject; + return res; + } +} + +export default KubernetesEventConverter; diff --git a/app/kubernetes/converters/namespace.js b/app/kubernetes/converters/namespace.js new file mode 100644 index 000000000..2a95a4c95 --- /dev/null +++ b/app/kubernetes/converters/namespace.js @@ -0,0 +1,29 @@ +import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; +import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads'; +import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; + +class KubernetesNamespaceConverter { + static apiToNamespace(data, yaml) { + const res = new KubernetesNamespace(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.CreationDate = data.metadata.creationTimestamp; + res.Status = data.status.phase; + res.Yaml = yaml ? yaml.data : ''; + res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; + res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : ''; + return res; + } + + static createPayload(namespace) { + const res = new KubernetesNamespaceCreatePayload(); + res.metadata.name = namespace.Name; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName; + if (namespace.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner; + } + return res; + } +} + +export default KubernetesNamespaceConverter; diff --git a/app/kubernetes/converters/node.js b/app/kubernetes/converters/node.js new file mode 100644 index 000000000..9a10f00c8 --- /dev/null +++ b/app/kubernetes/converters/node.js @@ -0,0 +1,65 @@ +import _ from 'lodash-es'; + +import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; + +class KubernetesNodeConverter { + static apiToNode(data, res) { + if (!res) { + res = new KubernetesNode(); + } + res.Id = data.metadata.uid; + const hostName = _.find(data.status.addresses, { type: 'Hostname' }); + res.Name = hostName ? hostName.address : data.metadata.Name; + res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Manager' : 'Worker'; + + const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY }); + const memoryPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.MEMORY_PRESSURE }); + const PIDPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.PID_PRESSURE }); + const diskPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.DISK_PRESSURE }); + const networkUnavailable = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.NETWORK_UNAVAILABLE }); + + res.Conditions = { + MemoryPressure: memoryPressure && memoryPressure.status === 'True', + PIDPressure: PIDPressure && PIDPressure.status === 'True', + DiskPressure: diskPressure && diskPressure.status === 'True', + NetworkUnavailable: networkUnavailable && networkUnavailable.status === 'True', + }; + + if (ready.status === 'False') { + res.Status = 'Unhealthy'; + } else if (ready.status === 'Unknown' || res.Conditions.MemoryPressure || res.Conditions.PIDPressure || res.Conditions.DiskPressure || res.Conditions.NetworkUnavailable) { + res.Status = 'Warning'; + } else { + res.Status = 'Ready'; + } + + res.CPU = KubernetesResourceReservationHelper.parseCPU(data.status.allocatable.cpu); + res.Memory = data.status.allocatable.memory; + res.Version = data.status.nodeInfo.kubeletVersion; + const internalIP = _.find(data.status.addresses, { type: 'InternalIP' }); + res.IPAddress = internalIP ? internalIP.address : '-'; + return res; + } + + static apiToNodeDetails(data, yaml) { + let res = new KubernetesNodeDetails(); + res = KubernetesNodeConverter.apiToNode(data, res); + res.CreationDate = data.metadata.creationTimestamp; + res.OS.Architecture = data.status.nodeInfo.architecture; + res.OS.Platform = data.status.nodeInfo.operatingSystem; + res.OS.Image = data.status.nodeInfo.osImage; + res.Yaml = yaml ? yaml.data : ''; + return res; + } +} + +export const KubernetesNodeConditionTypes = Object.freeze({ + READY: 'Ready', + MEMORY_PRESSURE: 'MemoryPressure', + PID_PRESSURE: 'PIDPressure', + DISK_PRESSURE: 'DiskPressure', + NETWORK_UNAVAILABLE: 'NetworkUnavailable', +}); + +export default KubernetesNodeConverter; diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js new file mode 100644 index 000000000..29838a86c --- /dev/null +++ b/app/kubernetes/converters/persistentVolumeClaim.js @@ -0,0 +1,68 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesPersistentVolumeClaim } from 'Kubernetes/models/volume/models'; +import { KubernetesPersistentVolumClaimCreatePayload } from 'Kubernetes/models/volume/payloads'; +import { KubernetesPortainerApplicationOwnerLabel, KubernetesPortainerApplicationNameLabel } from 'Kubernetes/models/application/models'; + +class KubernetesPersistentVolumeClaimConverter { + static apiToPersistentVolumeClaim(data, storageClasses, yaml) { + const res = new KubernetesPersistentVolumeClaim(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.CreationDate = data.metadata.creationTimestamp; + res.Storage = data.spec.resources.requests.storage.replace('i', '') + 'B'; + res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName }); + res.Yaml = yaml ? yaml.data : ''; + res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : ''; + res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : ''; + return res; + } + + /** + * Generate KubernetesPersistentVolumeClaim list from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToVolumeClaims(formValues) { + _.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion); + const res = _.map(formValues.PersistedFolders, (item) => { + const pvc = new KubernetesPersistentVolumeClaim(); + if (item.PersistentVolumeClaimName) { + pvc.Name = item.PersistentVolumeClaimName; + pvc.PreviousName = item.PersistentVolumeClaimName; + } else { + pvc.Name = formValues.Name + '-' + pvc.Name; + } + pvc.MountPath = item.ContainerPath; + pvc.Namespace = formValues.ResourcePool.Namespace.Name; + pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i'; + pvc.StorageClass = item.StorageClass; + pvc.ApplicationOwner = formValues.ApplicationOwner; + pvc.ApplicationName = formValues.Name; + return pvc; + }); + return res; + } + + static createPayload(pvc) { + const res = new KubernetesPersistentVolumClaimCreatePayload(); + res.metadata.name = pvc.Name; + res.metadata.namespace = pvc.Namespace; + res.spec.resources.requests.storage = pvc.Storage; + res.spec.storageClassName = pvc.StorageClass.Name; + res.metadata.labels.app = pvc.ApplicationName; + res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner; + res.metadata.labels[KubernetesPortainerApplicationNameLabel] = pvc.ApplicationName; + return res; + } + + static patchPayload(oldPVC, newPVC) { + const oldPayload = KubernetesPersistentVolumeClaimConverter.createPayload(oldPVC); + const newPayload = KubernetesPersistentVolumeClaimConverter.createPayload(newPVC); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesPersistentVolumeClaimConverter; diff --git a/app/kubernetes/converters/pod.js b/app/kubernetes/converters/pod.js new file mode 100644 index 000000000..af3d7713b --- /dev/null +++ b/app/kubernetes/converters/pod.js @@ -0,0 +1,32 @@ +import _ from 'lodash-es'; +import { KubernetesPod } from 'Kubernetes/models/pod/models'; +class KubernetesPodConverter { + static computeStatus(statuses) { + const containerStatuses = _.map(statuses, 'state'); + const running = _.filter(containerStatuses, (s) => s.running).length; + const waiting = _.filter(containerStatuses, (s) => s.waiting).length; + if (waiting) { + return 'Waiting'; + } else if (!running) { + return 'Terminated'; + } + return 'Running'; + } + + static apiToPod(data) { + const res = new KubernetesPod(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.Images = _.map(data.spec.containers, 'image'); + res.Status = KubernetesPodConverter.computeStatus(data.status.containerStatuses); + res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount'); + res.Node = data.spec.nodeName; + res.CreationDate = data.status.startTime; + res.Containers = data.spec.containers; + res.Labels = data.metadata.labels; + return res; + } +} + +export default KubernetesPodConverter; diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js new file mode 100644 index 000000000..daf2f002d --- /dev/null +++ b/app/kubernetes/converters/resourcePool.js @@ -0,0 +1,12 @@ +import { KubernetesResourcePool } from 'Kubernetes/models/resource-pool/models'; + +class KubernetesResourcePoolConverter { + static apiToResourcePool(namespace) { + const res = new KubernetesResourcePool(); + res.Namespace = namespace; + res.Yaml = namespace.Yaml; + return res; + } +} + +export default KubernetesResourcePoolConverter; diff --git a/app/kubernetes/converters/resourceQuota.js b/app/kubernetes/converters/resourceQuota.js new file mode 100644 index 000000000..5ba76276f --- /dev/null +++ b/app/kubernetes/converters/resourceQuota.js @@ -0,0 +1,87 @@ +import filesizeParser from 'filesize-parser'; + +import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; +import { KubernetesResourceQuotaCreatePayload, KubernetesResourceQuotaUpdatePayload } from 'Kubernetes/models/resource-quota/payloads'; +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; +import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; + +class KubernetesResourceQuotaConverter { + static apiToResourceQuota(data, yaml) { + const res = new KubernetesResourceQuota(); + res.Id = data.metadata.uid; + res.Namespace = data.metadata.namespace; + res.Name = data.metadata.name; + res.CpuLimit = 0; + res.MemoryLimit = 0; + if (data.spec.hard && data.spec.hard['limits.cpu']) { + res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard['limits.cpu']); + } + if (data.spec.hard && data.spec.hard['limits.memory']) { + res.MemoryLimit = filesizeParser(data.spec.hard['limits.memory'], { base: 10 }); + } + + res.MemoryLimitUsed = 0; + if (data.status.used && data.status.used['limits.memory']) { + res.MemoryLimitUsed = filesizeParser(data.status.used['limits.memory'], { base: 10 }); + } + + res.CpuLimitUsed = 0; + if (data.status.used && data.status.used['limits.cpu']) { + res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used['limits.cpu']); + } + res.Yaml = yaml ? yaml.data : ''; + res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; + res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : ''; + return res; + } + + static createPayload(quota) { + const res = new KubernetesResourceQuotaCreatePayload(); + res.metadata.name = KubernetesResourceQuotaHelper.generateResourceQuotaName(quota.Namespace); + res.metadata.namespace = quota.Namespace; + res.spec.hard['requests.cpu'] = quota.CpuLimit; + res.spec.hard['requests.memory'] = quota.MemoryLimit; + res.spec.hard['limits.cpu'] = quota.CpuLimit; + res.spec.hard['limits.memory'] = quota.MemoryLimit; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; + if (quota.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; + } + if (!quota.CpuLimit || quota.CpuLimit === 0) { + delete res.spec.hard['requests.cpu']; + delete res.spec.hard['limits.cpu']; + } + if (!quota.MemoryLimit || quota.MemoryLimit === 0) { + delete res.spec.hard['requests.memory']; + delete res.spec.hard['limits.memory']; + } + return res; + } + + static updatePayload(quota) { + const res = new KubernetesResourceQuotaUpdatePayload(); + res.metadata.name = quota.Name; + res.metadata.namespace = quota.Namespace; + res.metadata.uid = quota.Id; + res.spec.hard['requests.cpu'] = quota.CpuLimit; + res.spec.hard['requests.memory'] = quota.MemoryLimit; + res.spec.hard['limits.cpu'] = quota.CpuLimit; + res.spec.hard['limits.memory'] = quota.MemoryLimit; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; + if (quota.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; + } + if (!quota.CpuLimit || quota.CpuLimit === 0) { + delete res.spec.hard['requests.cpu']; + delete res.spec.hard['limits.cpu']; + } + if (!quota.MemoryLimit || quota.MemoryLimit === 0) { + delete res.spec.hard['requests.memory']; + delete res.spec.hard['limits.memory']; + } + return res; + } +} + +export default KubernetesResourceQuotaConverter; diff --git a/app/kubernetes/converters/secret.js b/app/kubernetes/converters/secret.js new file mode 100644 index 000000000..2c85352f0 --- /dev/null +++ b/app/kubernetes/converters/secret.js @@ -0,0 +1,58 @@ +import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads'; +import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models'; +import YAML from 'yaml'; +import _ from 'lodash-es'; +import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models'; + +class KubernetesSecretConverter { + static createPayload(secret) { + const res = new KubernetesSecretCreatePayload(); + res.metadata.name = secret.Name; + res.metadata.namespace = secret.Namespace; + res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner; + res.stringData = secret.Data; + return res; + } + + static updatePayload(secret) { + const res = new KubernetesSecretUpdatePayload(); + res.metadata.name = secret.Name; + res.metadata.namespace = secret.Namespace; + res.stringData = secret.Data; + return res; + } + + static apiToSecret(payload, yaml) { + const res = new KubernetesApplicationSecret(); + res.Id = payload.metadata.uid; + res.Name = payload.metadata.name; + res.Namespace = payload.metadata.namespace; + res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : ''; + res.CreationDate = payload.metadata.creationTimestamp; + res.Yaml = yaml ? yaml.data : ''; + res.Data = payload.data; + return res; + } + + static configurationFormValuesToSecret(formValues) { + const res = new KubernetesApplicationSecret(); + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.ConfigurationOwner = formValues.ConfigurationOwner; + if (formValues.IsSimple) { + res.Data = _.reduce( + formValues.Data, + (acc, entry) => { + acc[entry.Key] = entry.Value; + return acc; + }, + {} + ); + } else { + res.Data = YAML.parse(formValues.DataYaml); + } + return res; + } +} + +export default KubernetesSecretConverter; diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js new file mode 100644 index 000000000..a04c04ef0 --- /dev/null +++ b/app/kubernetes/converters/service.js @@ -0,0 +1,88 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, +} from 'Kubernetes/models/application/models'; +import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; +import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; + +class KubernetesServiceConverter { + static publishedPortToServicePort(name, publishedPort, type) { + const res = new KubernetesServicePort(); + res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol); + res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort; + res.targetPort = publishedPort.ContainerPort; + res.protocol = publishedPort.Protocol; + if (type === KubernetesServiceTypes.NODE_PORT && publishedPort.NodePort) { + res.nodePort = publishedPort.NodePort; + } else if (type === KubernetesServiceTypes.LOAD_BALANCER && publishedPort.LoadBalancerNodePort) { + res.nodePort = publishedPort.LoadBalancerNodePort; + } else { + delete res.nodePort; + } + return res; + } + + /** + * Generate KubernetesService from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToService(formValues) { + const res = new KubernetesService(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) { + res.Type = KubernetesServiceTypes.NODE_PORT; + } else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { + res.Type = KubernetesServiceTypes.LOAD_BALANCER; + } + res.Ports = _.map(formValues.PublishedPorts, (item) => KubernetesServiceConverter.publishedPortToServicePort(formValues.Name, item, res.Type)); + return res; + } + + static applicationFormValuesToHeadlessService(formValues) { + const res = KubernetesServiceConverter.applicationFormValuesToService(formValues); + res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name); + res.Headless = true; + return res; + } + + /** + * Generate CREATE payload from Service + * @param {KubernetesService} model Service to genereate payload from + */ + static createPayload(service) { + const payload = new KubernetesServiceCreatePayload(); + payload.metadata.name = service.Name; + payload.metadata.namespace = service.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner; + payload.spec.ports = service.Ports; + payload.spec.selector.app = service.ApplicationName; + if (service.Headless) { + payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP; + delete payload.spec.ports; + } else if (service.Type) { + payload.spec.type = service.Type; + } + return payload; + } + + static patchPayload(oldService, newService) { + const oldPayload = KubernetesServiceConverter.createPayload(oldService); + const newPayload = KubernetesServiceConverter.createPayload(newService); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesServiceConverter; diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js new file mode 100644 index 000000000..7867af0ee --- /dev/null +++ b/app/kubernetes/converters/statefulSet.js @@ -0,0 +1,84 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesStatefulSetCreatePayload } from 'Kubernetes/models/stateful-set/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationNote, +} from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim'; + +class KubernetesStatefulSetConverter { + /** + * Generate KubernetesStatefulSet from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToStatefulSet(formValues, volumeClaims) { + const res = new KubernetesStatefulSet(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.ReplicaCount = formValues.ReplicaCount; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from StatefulSet + * @param {KubernetesStatefulSetPayload} model StatefulSet to genereate payload from + */ + static createPayload(statefulSet) { + const payload = new KubernetesStatefulSetCreatePayload(); + payload.metadata.name = statefulSet.Name; + payload.metadata.namespace = statefulSet.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note; + payload.spec.replicas = statefulSet.ReplicaCount; + payload.spec.serviceName = statefulSet.ServiceName; + payload.spec.selector.matchLabels.app = statefulSet.Name; + payload.spec.volumeClaimTemplates = _.map(statefulSet.VolumeClaims, (item) => KubernetesPersistentVolumeClaimConverter.createPayload(item)); + payload.spec.template.metadata.labels.app = statefulSet.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; + payload.spec.template.spec.containers[0].name = statefulSet.Name; + payload.spec.template.spec.containers[0].image = statefulSet.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes); + if (statefulSet.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = statefulSet.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = statefulSet.MemoryLimit; + } + if (statefulSet.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = statefulSet.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = statefulSet.CpuLimit; + } + if (!statefulSet.CpuLimit && !statefulSet.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldSFS, newSFS) { + const oldPayload = KubernetesStatefulSetConverter.createPayload(oldSFS); + const newPayload = KubernetesStatefulSetConverter.createPayload(newSFS); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesStatefulSetConverter; diff --git a/app/kubernetes/converters/storageClass.js b/app/kubernetes/converters/storageClass.js new file mode 100644 index 000000000..2aad0f02e --- /dev/null +++ b/app/kubernetes/converters/storageClass.js @@ -0,0 +1,14 @@ +import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; + +class KubernetesStorageClassConverter { + /** + * API StorageClass to front StorageClass + */ + static apiToStorageClass(data) { + const res = new KubernetesStorageClass(); + res.Name = data.metadata.name; + return res; + } +} + +export default KubernetesStorageClassConverter; diff --git a/app/kubernetes/converters/volume.js b/app/kubernetes/converters/volume.js new file mode 100644 index 000000000..215c8437e --- /dev/null +++ b/app/kubernetes/converters/volume.js @@ -0,0 +1,12 @@ +import { KubernetesVolume } from 'Kubernetes/models/volume/models'; + +class KubernetesVolumeConverter { + static pvcToVolume(claim, pool) { + const res = new KubernetesVolume(); + res.PersistentVolumeClaim = claim; + res.ResourcePool = pool; + return res; + } +} + +export default KubernetesVolumeConverter; diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js new file mode 100644 index 000000000..968218e06 --- /dev/null +++ b/app/kubernetes/filters/applicationFilters.js @@ -0,0 +1,72 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesApplicationServiceTypeIcon', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'loadbalancer': + return 'fa-project-diagram'; + case 'clusterip': + return 'fa-list-alt'; + case 'nodeport': + return 'fa-list'; + } + }; + }) + .filter('kubernetesApplicationServiceTypeText', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'loadbalancer': + return 'Load balancer'; + case 'clusterip': + return 'Internal'; + case 'nodeport': + return 'Cluster'; + } + }; + }) + .filter('kubernetesApplicationCPUValue', function () { + 'use strict'; + return function (value) { + return _.round(value, 2); + }; + }) + .filter('kubernetesApplicationDataAccessPolicyIcon', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'fa-cubes'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'fa-cube'; + } + }; + }) + .filter('kubernetesApplicationDataAccessPolicyText', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'Isolated'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'Shared'; + } + }; + }) + .filter('kubernetesApplicationDataAccessPolicyTooltip', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'All the instances of this application are using their own data.'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'All the instances of this application are sharing the same data.'; + } + }; + }); diff --git a/app/kubernetes/filters/configurationFilters.js b/app/kubernetes/filters/configurationFilters.js new file mode 100644 index 000000000..9ba1e1d3a --- /dev/null +++ b/app/kubernetes/filters/configurationFilters.js @@ -0,0 +1,13 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +angular.module('portainer.kubernetes').filter('kubernetesConfigurationTypeText', function () { + 'use strict'; + return function (type) { + switch (type) { + case KubernetesConfigurationTypes.SECRET: + return 'Sensitive'; + case KubernetesConfigurationTypes.CONFIGMAP: + return 'Non-sensitive'; + } + }; +}); diff --git a/app/kubernetes/filters/eventFilters.js b/app/kubernetes/filters/eventFilters.js new file mode 100644 index 000000000..84148945e --- /dev/null +++ b/app/kubernetes/filters/eventFilters.js @@ -0,0 +1,16 @@ +import _ from 'lodash-es'; + +angular.module('portainer.kubernetes').filter('kubernetesEventTypeColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'normal': + return 'info'; + case 'warning': + return 'warning'; + default: + return 'danger'; + } + }; +}); diff --git a/app/kubernetes/filters/filters.js b/app/kubernetes/filters/filters.js new file mode 100644 index 000000000..6fcb031b1 --- /dev/null +++ b/app/kubernetes/filters/filters.js @@ -0,0 +1,11 @@ +angular.module('portainer.kubernetes').filter('kubernetesUsageLevelInfo', function () { + return function (usage) { + if (usage >= 80) { + return 'danger'; + } else if (usage > 50 && usage < 80) { + return 'warning'; + } else { + return 'success'; + } + }; +}); diff --git a/app/kubernetes/filters/nodeFilters.js b/app/kubernetes/filters/nodeFilters.js new file mode 100644 index 000000000..d848a9515 --- /dev/null +++ b/app/kubernetes/filters/nodeFilters.js @@ -0,0 +1,35 @@ +import _ from 'lodash-es'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesNodeStatusColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'ready': + return 'success'; + case 'warning': + return 'warning'; + default: + return 'danger'; + } + }; + }) + .filter('kubernetesNodeConditionsMessage', function () { + 'use strict'; + return function (conditions) { + if (conditions.MemoryPressure) { + return 'Node memory is running low'; + } + if (conditions.PIDPressure) { + return 'Too many processes running on the node'; + } + if (conditions.DiskPressure) { + return 'Node disk capacity is running low'; + } + if (conditions.NetworkUnavailable) { + return 'Incorrect node network configuration'; + } + }; + }); diff --git a/app/kubernetes/filters/podFilters.js b/app/kubernetes/filters/podFilters.js new file mode 100644 index 000000000..6d740636d --- /dev/null +++ b/app/kubernetes/filters/podFilters.js @@ -0,0 +1,84 @@ +import _ from 'lodash-es'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesPodStatusColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'running': + return 'success'; + case 'waiting': + return 'warning'; + case 'terminated': + return 'info'; + default: + return 'danger'; + } + }; + }) + .filter('kubernetesPodConditionStatusBadge', function () { + 'use strict'; + return function (status, type) { + switch (type) { + case 'Unschedulable': + switch (status) { + case 'True': + return 'fa-exclamation-triangle red-icon'; + case 'False': + return 'fa-check green-icon'; + case 'Unknown': + return 'fa-exclamation-circle orange-icon'; + } + break; + case 'PodScheduled': + case 'Ready': + case 'Initialized': + case 'ContainersReady': + switch (status) { + case 'True': + return 'fa-check green-icon'; + case 'False': + return 'fa-exclamation-triangle red-icon'; + case 'Unknown': + return 'fa-exclamation-circle orange-icon'; + } + break; + default: + return 'fa-question-circle red-icon'; + } + }; + }) + .filter('kubernetesPodConditionStatusText', function () { + 'use strict'; + return function (status, type) { + switch (type) { + case 'Unschedulable': + switch (status) { + case 'True': + return 'Alert'; + case 'False': + return 'OK'; + case 'Unknown': + return 'Warning'; + } + break; + case 'PodScheduled': + case 'Ready': + case 'Initialized': + case 'ContainersReady': + switch (status) { + case 'True': + return 'Ok'; + case 'False': + return 'Alert'; + case 'Unknown': + return 'Warning'; + } + break; + default: + return 'Unknown'; + } + }; + }); diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js new file mode 100644 index 000000000..ab5c43b78 --- /dev/null +++ b/app/kubernetes/helpers/application/index.js @@ -0,0 +1,273 @@ +import _ from 'lodash-es'; +import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; +import { + KubernetesApplicationConfigurationFormValueOverridenKeyTypes, + KubernetesApplicationEnvironmentVariableFormValue, + KubernetesApplicationConfigurationFormValue, + KubernetesApplicationConfigurationFormValueOverridenKey, + KubernetesApplicationPersistedFolderFormValue, + KubernetesApplicationPublishedPortFormValue, +} from 'Kubernetes/models/application/formValues'; +import { + KubernetesApplicationEnvConfigMapPayload, + KubernetesApplicationEnvPayload, + KubernetesApplicationEnvSecretPayload, + KubernetesApplicationVolumeConfigMapPayload, + KubernetesApplicationVolumeEntryPayload, + KubernetesApplicationVolumeMountPayload, + KubernetesApplicationVolumePersistentPayload, + KubernetesApplicationVolumeSecretPayload, +} from 'Kubernetes/models/application/payloads'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; + +class KubernetesApplicationHelper { + static associatePodsAndApplication(pods, app) { + return _.filter(pods, { Labels: app.spec.selector.matchLabels }); + } + + static portMappingsFromApplications(applications) { + const res = _.reduce( + applications, + (acc, app) => { + if (app.PublishedPorts.length > 0) { + const mapping = new KubernetesPortMapping(); + mapping.Name = app.Name; + mapping.ResourcePool = app.ResourcePool; + mapping.ServiceType = app.ServiceType; + mapping.LoadBalancerIPAddress = app.LoadBalancerIPAddress; + mapping.ApplicationOwner = app.ApplicationOwner; + + mapping.Ports = _.map(app.PublishedPorts, (item) => { + const port = new KubernetesPortMappingPort(); + port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.nodePort : item.port; + port.TargetPort = item.targetPort; + port.Protocol = item.protocol; + return port; + }); + acc.push(mapping); + } + return acc; + }, + [] + ); + return res; + } + + /** + * FORMVALUES TO APPLICATION FUNCTIONS + */ + static generateEnvFromEnvVariables(envVariables) { + _.remove(envVariables, (item) => item.NeedsDeletion); + const env = _.map(envVariables, (item) => { + const res = new KubernetesApplicationEnvPayload(); + res.name = item.Name; + res.value = item.Value; + return res; + }); + return env; + } + + static generateEnvOrVolumesFromConfigurations(app, configurations) { + let finalEnv = []; + let finalVolumes = []; + let finalMounts = []; + + _.forEach(configurations, (config) => { + const isBasic = config.SelectedConfiguration.Type === KubernetesConfigurationTypes.CONFIGMAP; + + if (!config.Overriden) { + const envKeys = _.keys(config.SelectedConfiguration.Data); + _.forEach(envKeys, (item) => { + const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload(); + res.name = item; + if (isBasic) { + res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.configMapKeyRef.key = item; + } else { + res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.secretKeyRef.key = item; + } + finalEnv.push(res); + }); + } else { + const envKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT); + _.forEach(envKeys, (item) => { + const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload(); + res.name = item.Key; + if (isBasic) { + res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.configMapKeyRef.key = item.Key; + } else { + res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.secretKeyRef.key = item.Key; + } + finalEnv.push(res); + }); + + const volKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM); + const groupedVolKeys = _.groupBy(volKeys, 'Path'); + _.forEach(groupedVolKeys, (items, path) => { + const volumeName = KubernetesVolumeHelper.generatedApplicationConfigVolumeName(app.Name); + const configurationName = config.SelectedConfiguration.Name; + const itemsMap = _.map(items, (item) => { + const entry = new KubernetesApplicationVolumeEntryPayload(); + entry.key = item.Key; + entry.path = item.Key; + return entry; + }); + + const mount = isBasic ? new KubernetesApplicationVolumeMountPayload() : new KubernetesApplicationVolumeMountPayload(true); + const volume = isBasic ? new KubernetesApplicationVolumeConfigMapPayload() : new KubernetesApplicationVolumeSecretPayload(); + + mount.name = volumeName; + mount.mountPath = path; + volume.name = volumeName; + if (isBasic) { + volume.configMap.name = configurationName; + volume.configMap.items = itemsMap; + } else { + volume.secret.secretName = configurationName; + volume.secret.items = itemsMap; + } + + finalMounts.push(mount); + finalVolumes.push(volume); + }); + } + }); + app.Env = _.concat(app.Env, finalEnv); + app.Volumes = _.concat(app.Volumes, finalVolumes); + app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts); + return app; + } + + static generateVolumesFromPersistentVolumClaims(app, volumeClaims) { + app.VolumeMounts = []; + app.Volumes = []; + _.forEach(volumeClaims, (item) => { + const volumeMount = new KubernetesApplicationVolumeMountPayload(); + const name = item.Name; + volumeMount.name = name; + volumeMount.mountPath = item.MountPath; + app.VolumeMounts.push(volumeMount); + + const volume = new KubernetesApplicationVolumePersistentPayload(); + volume.name = name; + volume.persistentVolumeClaim.claimName = name; + app.Volumes.push(volume); + }); + } + /** + * !FORMVALUES TO APPLICATION FUNCTIONS + */ + + /** + * APPLICATION TO FORMVALUES FUNCTIONS + */ + static generateEnvVariablesFromEnv(env) { + const envVariables = _.map(env, (item) => { + if (!item.value) { + return; + } + const res = new KubernetesApplicationEnvironmentVariableFormValue(); + res.Name = item.name; + res.Value = item.value; + res.IsNew = false; + return res; + }); + return _.without(envVariables, undefined); + } + + static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) { + const finalRes = _.flatMap(configurations, (cfg) => { + const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name'; + + const cfgEnv = _.filter(env, [filterCondition, cfg.Name]); + const cfgVol = _.filter(volumes, { configurationName: cfg.Name }); + if (!cfgEnv.length && !cfgVol.length) { + return; + } + const keys = _.reduce( + _.keys(cfg.Data), + (acc, k) => { + const keyEnv = _.filter(cfgEnv, { name: k }); + const keyVol = _.filter(cfgVol, { configurationKey: k }); + const key = { + Key: k, + Count: keyEnv.length + keyVol.length, + Sum: _.concat(keyEnv, keyVol), + EnvCount: keyEnv.length, + VolCount: keyVol.length, + }; + acc.push(key); + return acc; + }, + [] + ); + + const max = _.max(_.map(keys, 'Count')); + const overrideThreshold = max - _.max(_.map(keys, 'VolCount')); + const res = _.map(new Array(max), () => new KubernetesApplicationConfigurationFormValue()); + _.forEach(res, (item, index) => { + item.SelectedConfiguration = cfg; + const overriden = index >= overrideThreshold; + if (overriden) { + item.Overriden = true; + item.OverridenKeys = _.map(keys, (k) => { + const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey(); + fvKey.Key = k.Key; + if (index < k.EnvCount) { + fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT; + } else { + fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM; + fvKey.Path = k.Sum[index].rootMountPath; + } + return fvKey; + }); + } + }); + return res; + }); + return _.without(finalRes, undefined); + } + + static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) { + const finalRes = _.map(persistedFolders, (folder) => { + const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName)); + const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass); + res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName; + res.Size = parseInt(pvc.Storage.slice(0, -2)); + res.SizeUnit = pvc.Storage.slice(-2); + res.ContainerPath = folder.MountPath; + return res; + }); + return finalRes; + } + + static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) { + const finalRes = _.map(publishedPorts, (port) => { + const res = new KubernetesApplicationPublishedPortFormValue(); + res.Protocol = port.protocol; + res.ContainerPort = port.targetPort; + if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { + res.LoadBalancerPort = port.port; + res.LoadBalancerNodePort = port.nodePort; + } else if (serviceType === KubernetesServiceTypes.NODE_PORT) { + res.NodePort = port.nodePort; + } + return res; + }); + return finalRes; + } + + /** + * !APPLICATION TO FORMVALUES FUNCTIONS + */ + + static isExternalApplication(application) { + return !application.ApplicationOwner; + } +} +export default KubernetesApplicationHelper; diff --git a/app/kubernetes/helpers/application/rollback.js b/app/kubernetes/helpers/application/rollback.js new file mode 100644 index 000000000..f593e0cf5 --- /dev/null +++ b/app/kubernetes/helpers/application/rollback.js @@ -0,0 +1,76 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; + +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_AnnotationsToSkip } from 'Kubernetes/models/history/models'; + +class KubernetesApplicationRollbackHelper { + static getPatchPayload(application, targetRevision) { + let result; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + result = KubernetesApplicationRollbackHelper._getDeploymentPayload(application, targetRevision); + break; + case KubernetesApplicationTypes.DAEMONSET: + result = KubernetesApplicationRollbackHelper._getDaemonSetPayload(application, targetRevision); + break; + case KubernetesApplicationTypes.STATEFULSET: + result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + return result; + } + + static _getDeploymentPayload(deploymentApp, targetRevision) { + const target = angular.copy(targetRevision); + const deployment = deploymentApp.Raw; + + // remove hash label before patching back into the deployment + delete target.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey]; + + // compute deployment annotations + const annotations = {}; + _.forEach(KubernetesSystem_AnnotationsToSkip, (_, k) => { + const v = deployment.metadata.annotations[k]; + if (v) { + annotations[k] = v; + } + }); + _.forEach(target.metadata.annotations, (v, k) => { + if (!KubernetesSystem_AnnotationsToSkip[k]) { + annotations[k] = v; + } + }); + // Create a patch of the Deployment that replaces spec.template + const patch = [ + { + op: 'replace', + path: '/spec/template', + value: target.spec.template, + }, + { + op: 'replace', + path: '/metadata/annotations', + value: annotations, + }, + ]; + + return patch; + } + + static _getDaemonSetPayload(daemonSet, targetRevision) { + void daemonSet; + return targetRevision.data; + } + + static _getStatefulSetPayload(statefulSet, targetRevision) { + void statefulSet; + return targetRevision.data; + } +} + +export default KubernetesApplicationRollbackHelper; diff --git a/app/kubernetes/helpers/commonHelper.js b/app/kubernetes/helpers/commonHelper.js new file mode 100644 index 000000000..af461775f --- /dev/null +++ b/app/kubernetes/helpers/commonHelper.js @@ -0,0 +1,12 @@ +import _ from 'lodash-es'; + +class KubernetesCommonHelper { + static assignOrDeleteIfEmpty(obj, path, value) { + if (!value || (value instanceof Array && !value.length)) { + _.unset(obj, path); + } else { + _.set(obj, path, value); + } + } +} +export default KubernetesCommonHelper; diff --git a/app/kubernetes/helpers/configMapHelper.js b/app/kubernetes/helpers/configMapHelper.js new file mode 100644 index 000000000..6e4eead4e --- /dev/null +++ b/app/kubernetes/helpers/configMapHelper.js @@ -0,0 +1,36 @@ +import _ from 'lodash-es'; + +import { KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models'; +import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access'; + +class KubernetesConfigMapHelper { + static parseJSONData(configMap) { + _.forIn(configMap.Data, (value, key) => { + try { + configMap.Data[key] = JSON.parse(value); + } catch (err) { + configMap.Data[key] = value; + } + }); + return configMap; + } + + static modifiyNamespaceAccesses(configMap, namespace, accesses) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace] = { + UserAccessPolicies: {}, + TeamAccessPolicies: {}, + }; + _.forEach(accesses, (item) => { + if (item instanceof UserAccessViewModel) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].UserAccessPolicies[item.Id] = { RoleId: 0 }; + } else if (item instanceof TeamAccessViewModel) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].TeamAccessPolicies[item.Id] = { RoleId: 0 }; + } + }); + _.forIn(configMap.Data, (value, key) => { + configMap.Data[key] = JSON.stringify(value); + }); + return configMap; + } +} +export default KubernetesConfigMapHelper; diff --git a/app/kubernetes/helpers/configurationHelper.js b/app/kubernetes/helpers/configurationHelper.js new file mode 100644 index 000000000..ee8acb9e4 --- /dev/null +++ b/app/kubernetes/helpers/configurationHelper.js @@ -0,0 +1,40 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; +import _ from 'lodash-es'; + +class KubernetesConfigurationHelper { + static getUsingApplications(config, applications) { + return _.filter(applications, (app) => { + let envFind; + let volumeFind; + if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { + envFind = _.find(app.Env, { valueFrom: { configMapKeyRef: { name: config.Name } } }); + volumeFind = _.find(app.Volumes, { configMap: { name: config.Name } }); + } else { + envFind = _.find(app.Env, { valueFrom: { secretKeyRef: { name: config.Name } } }); + volumeFind = _.find(app.Volumes, { secret: { secretName: config.Name } }); + } + return envFind || volumeFind; + }); + } + + static isSystemToken(config) { + return _.startsWith(config.Name, 'default-token-'); + } + + static setConfigurationUsed(config) { + config.Used = config.Applications && config.Applications.length !== 0; + } + + static setConfigurationsUsed(configurations, applications) { + _.forEach(configurations, (config) => { + config.Applications = KubernetesConfigurationHelper.getUsingApplications(config, applications); + KubernetesConfigurationHelper.setConfigurationUsed(config); + }); + } + + static isExternalConfiguration(configuration) { + return !configuration.ConfigurationOwner; + } +} + +export default KubernetesConfigurationHelper; diff --git a/app/kubernetes/helpers/eventHelper.js b/app/kubernetes/helpers/eventHelper.js new file mode 100644 index 000000000..7cc0cc865 --- /dev/null +++ b/app/kubernetes/helpers/eventHelper.js @@ -0,0 +1,10 @@ +import _ from 'lodash-es'; + +class KubernetesEventHelper { + static warningCount(events) { + const warnings = _.filter(events, (event) => event.Type === 'Warning'); + return warnings.length; + } +} + +export default KubernetesEventHelper; diff --git a/app/kubernetes/helpers/formValidationHelper.js b/app/kubernetes/helpers/formValidationHelper.js new file mode 100644 index 000000000..cb506e366 --- /dev/null +++ b/app/kubernetes/helpers/formValidationHelper.js @@ -0,0 +1,15 @@ +import _ from 'lodash-es'; + +class KubernetesFormValidationHelper { + static getDuplicates(names) { + const groupped = _.groupBy(names); + const res = {}; + _.forEach(names, (name, index) => { + if (groupped[name].length > 1 && name) { + res[index] = name; + } + }); + return res; + } +} +export default KubernetesFormValidationHelper; diff --git a/app/kubernetes/helpers/history/daemonset.js b/app/kubernetes/helpers/history/daemonset.js new file mode 100644 index 000000000..506a0f0b1 --- /dev/null +++ b/app/kubernetes/helpers/history/daemonset.js @@ -0,0 +1,27 @@ +import _ from 'lodash-es'; + +class KubernetesDaemonSetHistoryHelper { + static _isControlledBy(daemonSet) { + return (item) => _.find(item.metadata.ownerReferences, { uid: daemonSet.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(crList, daemonSet) { + // filter ControllerRevisions that has the same selector as the DaemonSet + // NOTE : this should be done in HTTP request based on daemonSet.spec.selector.matchLabels + // instead of getting all CR and filtering them here + const sameLabelsCR = _.filter(crList, ['metadata.labels', daemonSet.spec.selector.matchLabels]); + // Only include the RS whose ControllerRef matches the DaemonSet. + const controlledCR = _.filter(sameLabelsCR, KubernetesDaemonSetHistoryHelper._isControlledBy(daemonSet)); + // sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new) + const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']); + return sortedList; + } + + // getCurrentRS returns the newest CR the given daemonSet targets (latest version) + static getCurrentRevision(crList) { + const current = _.last(crList); + return current; + } +} + +export default KubernetesDaemonSetHistoryHelper; diff --git a/app/kubernetes/helpers/history/deployment.js b/app/kubernetes/helpers/history/deployment.js new file mode 100644 index 000000000..96974eedc --- /dev/null +++ b/app/kubernetes/helpers/history/deployment.js @@ -0,0 +1,56 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_RevisionAnnotation } from 'Kubernetes/models/history/models'; + +class KubernetesDeploymentHistoryHelper { + static _isControlledBy(deployment) { + return (item) => _.find(item.metadata.ownerReferences, { uid: deployment.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(rsList, deployment) { + // filter RS that has the same selector as the Deployment + // NOTE : this should be done in HTTP request based on deployment.spec.selector + // instead of getting all RS and filtering them here + const sameLabelsRS = _.filter(rsList, ['spec.selector', deployment.spec.selector]); + // Only include the RS whose ControllerRef matches the Deployment. + const controlledRS = _.filter(sameLabelsRS, KubernetesDeploymentHistoryHelper._isControlledBy(deployment)); + // sorts the list of ReplicaSet by creation timestamp, using the names as a tie breaker (old to new) + const sortedList = _.sortBy(controlledRS, ['metadata.creationTimestamp', 'metadata.name']); + return sortedList; + } + + // getCurrentRS returns the new RS the given deployment targets (the one with the same pod template). + static getCurrentRevision(rsListOriginal, deployment) { + const rsList = angular.copy(rsListOriginal); + + // In rare cases, such as after cluster upgrades, Deployment may end up with + // having more than one new ReplicaSets that have the same template as its template, + // see https://github.com/kubernetes/kubernetes/issues/40415 + // We deterministically choose the oldest new ReplicaSet (first match) + const current = _.find(rsList, (item) => { + // returns true if two given template.spec are equal, ignoring the diff in value of Labels[pod-template-hash] + // We ignore pod-template-hash because: + // 1. The hash result would be different upon podTemplateSpec API changes + // (e.g. the addition of a new field will cause the hash code to change) + // 2. The deployment template won't have hash labels + delete item.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey]; + return _.isEqual(deployment.spec.template, item.spec.template); + }); + current.revision = current.metadata.annotations[KubernetesSystem_RevisionAnnotation]; + return current; + } + + // filters the RSList to drop all RS that have never been a version of the Deployment + // also add the revision as a field inside the RS + // Note: this should not impact rollback process as we only patch + // metadata.annotations and spec.template + static filterVersionedRevisions(rsList) { + const filteredRS = _.filter(rsList, (item) => item.metadata.annotations[KubernetesSystem_RevisionAnnotation] !== undefined); + return _.map(filteredRS, (item) => { + item.revision = item.metadata.annotations[KubernetesSystem_RevisionAnnotation]; + return item; + }); + } +} + +export default KubernetesDeploymentHistoryHelper; diff --git a/app/kubernetes/helpers/history/index.js b/app/kubernetes/helpers/history/index.js new file mode 100644 index 000000000..d611644a0 --- /dev/null +++ b/app/kubernetes/helpers/history/index.js @@ -0,0 +1,50 @@ +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; + +import KubernetesDeploymentHistoryHelper from 'Kubernetes/helpers/history/deployment'; +import KubernetesDaemonSetHistoryHelper from 'Kubernetes/helpers/history/daemonset'; +import KubernetesStatefulSetHistoryHelper from 'Kubernetes/helpers/history/statefulset'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesHistoryHelper { + static getRevisions(rawRevisions, application) { + let currentRevision, revisionsList; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getDeploymentRevisions(rawRevisions, application.Raw); + break; + case KubernetesApplicationTypes.DAEMONSET: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getDaemonSetRevisions(rawRevisions, application.Raw); + break; + case KubernetesApplicationTypes.STATEFULSET: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + revisionsList = _.sortBy(revisionsList, 'revision'); + return [currentRevision, revisionsList]; + } + + static _getDeploymentRevisions(rsList, deployment) { + const appRS = KubernetesDeploymentHistoryHelper.filterOwnedRevisions(rsList, deployment); + const currentRS = KubernetesDeploymentHistoryHelper.getCurrentRevision(appRS, deployment); + const versionedRS = KubernetesDeploymentHistoryHelper.filterVersionedRevisions(appRS); + return [currentRS, versionedRS]; + } + + static _getDaemonSetRevisions(crList, daemonSet) { + const appCR = KubernetesDaemonSetHistoryHelper.filterOwnedRevisions(crList, daemonSet); + const currentCR = KubernetesDaemonSetHistoryHelper.getCurrentRevision(appCR, daemonSet); + return [currentCR, appCR]; + } + + static _getStatefulSetRevisions(crList, statefulSet) { + const appCR = KubernetesStatefulSetHistoryHelper.filterOwnedRevisions(crList, statefulSet); + const currentCR = KubernetesStatefulSetHistoryHelper.getCurrentRevision(appCR, statefulSet); + return [currentCR, appCR]; + } +} + +export default KubernetesHistoryHelper; diff --git a/app/kubernetes/helpers/history/statefulset.js b/app/kubernetes/helpers/history/statefulset.js new file mode 100644 index 000000000..829cf0181 --- /dev/null +++ b/app/kubernetes/helpers/history/statefulset.js @@ -0,0 +1,27 @@ +import _ from 'lodash-es'; + +class KubernetesStatefulSetHistoryHelper { + static _isControlledBy(statefulSet) { + return (item) => _.find(item.metadata.ownerReferences, { uid: statefulSet.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(crList, statefulSet) { + // filter ControllerRevisions that has the same selector as the StatefulSet + // NOTE : this should be done in HTTP request based on statefulSet.spec.selector.matchLabels + // instead of getting all CR and filtering them here + const sameLabelsCR = _.filter(crList, ['metadata.labels', statefulSet.spec.selector.matchLabels]); + // Only include the RS whose ControllerRef matches the StatefulSet. + const controlledCR = _.filter(sameLabelsCR, KubernetesStatefulSetHistoryHelper._isControlledBy(statefulSet)); + // sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new) + const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']); + return sortedList; + } + + // getCurrentRS returns the newest CR the given statefulSet targets (latest version) + static getCurrentRevision(crList) { + const current = _.last(crList); + return current; + } +} + +export default KubernetesStatefulSetHistoryHelper; diff --git a/app/kubernetes/helpers/namespaceHelper.js b/app/kubernetes/helpers/namespaceHelper.js new file mode 100644 index 000000000..5050590d7 --- /dev/null +++ b/app/kubernetes/helpers/namespaceHelper.js @@ -0,0 +1,15 @@ +import _ from 'lodash-es'; +import angular from 'angular'; + +class KubernetesNamespaceHelper { + constructor(KUBERNETES_SYSTEM_NAMESPACES) { + this.KUBERNETES_SYSTEM_NAMESPACES = KUBERNETES_SYSTEM_NAMESPACES; + } + + isSystemNamespace(namespace) { + return _.includes(this.KUBERNETES_SYSTEM_NAMESPACES, namespace); + } +} + +export default KubernetesNamespaceHelper; +angular.module('portainer.app').service('KubernetesNamespaceHelper', KubernetesNamespaceHelper); diff --git a/app/kubernetes/helpers/resourceQuotaHelper.js b/app/kubernetes/helpers/resourceQuotaHelper.js new file mode 100644 index 000000000..add667dd7 --- /dev/null +++ b/app/kubernetes/helpers/resourceQuotaHelper.js @@ -0,0 +1,9 @@ +import { KubernetesPortainerResourceQuotaPrefix } from 'Kubernetes/models/resource-quota/models'; + +class KubernetesResourceQuotaHelper { + static generateResourceQuotaName(name) { + return KubernetesPortainerResourceQuotaPrefix + name; + } +} + +export default KubernetesResourceQuotaHelper; diff --git a/app/kubernetes/helpers/resourceReservationHelper.js b/app/kubernetes/helpers/resourceReservationHelper.js new file mode 100644 index 000000000..df24ee494 --- /dev/null +++ b/app/kubernetes/helpers/resourceReservationHelper.js @@ -0,0 +1,44 @@ +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; + +class KubernetesResourceReservationHelper { + static computeResourceReservation(pods) { + const containers = _.reduce(pods, (acc, pod) => _.concat(acc, pod.Containers), []); + + return _.reduce( + containers, + (acc, container) => { + if (container.resources && container.resources.requests) { + if (container.resources.requests.memory) { + acc.Memory += filesizeParser(container.resources.requests.memory, { base: 10 }); + } + + if (container.resources.requests.cpu) { + acc.CPU += KubernetesResourceReservationHelper.parseCPU(container.resources.requests.cpu); + } + } + + return acc; + }, + new KubernetesResourceReservation() + ); + } + + static parseCPU(cpu) { + let res = parseInt(cpu); + if (_.endsWith(cpu, 'm')) { + res /= 1000; + } + return res; + } + + static megaBytesValue(value) { + return Math.floor(filesizeParser(value) / 1000 / 1000); + } + + static bytesValue(mem) { + return filesizeParser(mem) * 1000 * 1000; + } +} +export default KubernetesResourceReservationHelper; diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js new file mode 100644 index 000000000..c4b35050d --- /dev/null +++ b/app/kubernetes/helpers/serviceHelper.js @@ -0,0 +1,13 @@ +import _ from 'lodash-es'; +import { KubernetesServiceHeadlessPrefix } from 'Kubernetes/models/service/models'; + +class KubernetesServiceHelper { + static generateHeadlessServiceName(name) { + return KubernetesServiceHeadlessPrefix + name; + } + + static findApplicationBoundService(services, rawApp) { + return _.find(services, (item) => _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); + } +} +export default KubernetesServiceHelper; diff --git a/app/kubernetes/helpers/stackHelper.js b/app/kubernetes/helpers/stackHelper.js new file mode 100644 index 000000000..73955862b --- /dev/null +++ b/app/kubernetes/helpers/stackHelper.js @@ -0,0 +1,26 @@ +import _ from 'lodash-es'; +import { KubernetesStack } from 'Kubernetes/models/stack/models'; + +class KubernetesStackHelper { + static stacksFromApplications(applications) { + const res = _.reduce( + applications, + (acc, app) => { + if (app.StackName !== '-') { + let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool }); + if (!stack) { + stack = new KubernetesStack(); + stack.Name = app.StackName; + stack.ResourcePool = app.ResourcePool; + acc.push(stack); + } + stack.Applications.push(app); + } + return acc; + }, + [] + ); + return res; + } +} +export default KubernetesStackHelper; diff --git a/app/kubernetes/helpers/volumeHelper.js b/app/kubernetes/helpers/volumeHelper.js new file mode 100644 index 000000000..c357ffb14 --- /dev/null +++ b/app/kubernetes/helpers/volumeHelper.js @@ -0,0 +1,37 @@ +import _ from 'lodash-es'; +import uuidv4 from 'uuid/v4'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesVolumeHelper { + // TODO: review + // the following condition + // && (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true); + // is made to enforce finding the good SFS when multiple SFS in the same namespace + // are referencing an internal PVC using the same internal name + // (PVC are not exposed to other apps so they can have the same name in differents SFS) + static getUsingApplications(volume, applications) { + return _.filter(applications, (app) => { + const names = _.without(_.map(app.Volumes, 'persistentVolumeClaim.claimName'), undefined); + const matchingNames = _.filter(names, (name) => _.startsWith(volume.PersistentVolumeClaim.Name, name)); + return ( + volume.ResourcePool.Namespace.Name === app.ResourcePool && + matchingNames.length && + (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true) + ); + }); + } + + static isUsed(item) { + return item.Applications.length !== 0; + } + + static generatedApplicationConfigVolumeName(name) { + return 'config-' + name + '-' + uuidv4(); + } + + static isExternalVolume(volume) { + return !volume.PersistentVolumeClaim.ApplicationOwner; + } +} + +export default KubernetesVolumeHelper; diff --git a/app/kubernetes/horizontal-pod-auto-scaler/converter.js b/app/kubernetes/horizontal-pod-auto-scaler/converter.js new file mode 100644 index 000000000..4c5ccea75 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/converter.js @@ -0,0 +1,23 @@ +import { KubernetesHorizontalPodAutoScaler } from './models'; + +export class KubernetesHorizontalPodAutoScalerConverter { + /** + * Convert API data to KubernetesHorizontalPodAutoScaler model + */ + static apiToModel(data, yaml) { + const res = new KubernetesHorizontalPodAutoScaler(); + res.Id = data.metadata.uid; + res.Namespace = data.metadata.namespace; + res.Name = data.metadata.name; + res.MinReplicas = data.spec.minReplicas; + res.MaxReplicas = data.spec.maxReplicas; + res.TargetCPUUtilizationPercentage = data.spec.targetCPUUtilizationPercentage; + if (data.spec.scaleTargetRef) { + res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; + res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; + res.TargetEntity.Name = data.spec.scaleTargetRef.name; + } + res.Yaml = yaml ? yaml.data : ''; + return res; + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/helper.js b/app/kubernetes/horizontal-pod-auto-scaler/helper.js new file mode 100644 index 000000000..af8061b85 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/helper.js @@ -0,0 +1,26 @@ +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import { KubernetesApplication, KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; + +function _getApplicationTypeString(app) { + if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { + return KubernetesApplicationTypeStrings.DEPLOYMENT; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { + return KubernetesApplicationTypeStrings.DAEMONSET; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { + return KubernetesApplicationTypeStrings.STATEFULSET; + // } else if () { ---> TODO: refactor - handle bare pod type ! + } else { + throw new PortainerError('Unable to determine application type'); + } +} + +export class KubernetesHorizontalPodAutoScalerHelper { + static findApplicationBoundScaler(sList, app) { + const kind = _getApplicationTypeString(app); + return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name); + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/models.js b/app/kubernetes/horizontal-pod-auto-scaler/models.js new file mode 100644 index 000000000..1cba85d7d --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/models.js @@ -0,0 +1,23 @@ +/** + * KubernetesHorizontalPodAutoScaler Model + */ +const _KubernetesHorizontalPodAutoScaler = Object.freeze({ + Id: '', + Namespace: '', + Name: '', + MinReplicas: 1, + MaxReplicas: 1, + TargetCPUUtilizationPercentage: undefined, + TargetEntity: { + ApiVersion: '', + Kind: '', + Name: '', + }, + Yaml: '', +}); + +export class KubernetesHorizontalPodAutoScaler { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScaler))); + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/rest.js b/app/kubernetes/horizontal-pod-auto-scaler/rest.js new file mode 100644 index 000000000..a6cdc4cb1 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/rest.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScalers', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesHorizontalPodAutoScalersFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/autoscaling/v1' + (namespace ? '/namespaces/:namespace' : '') + '/horizontalpodautoscalers/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/horizontal-pod-auto-scaler/service.js b/app/kubernetes/horizontal-pod-auto-scaler/service.js new file mode 100644 index 000000000..df33457ce --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/service.js @@ -0,0 +1,135 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import { KubernetesHorizontalPodAutoScalerConverter } from './converter'; + +class KubernetesHorizontalPodAutoScalerService { + /* @ngInject */ + constructor($async, KubernetesHorizontalPodAutoScalers) { + this.$async = $async; + this.KubernetesHorizontalPodAutoScalers = KubernetesHorizontalPodAutoScalers; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + // this.createAsync = this.createAsync.bind(this); + // this.patchAsync = this.patchAsync.bind(this); + // this.rollbackAsync = this.rollbackAsync.bind(this); + // this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([ + this.KubernetesHorizontalPodAutoScalers(namespace).get(params).$promise, + this.KubernetesHorizontalPodAutoScalers(namespace).getYaml(params).$promise, + ]); + const res = KubernetesHorizontalPodAutoScalerConverter.apiToModel(raw, yaml); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve HorizontalPodAutoScaler', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).get().$promise; + const res = _.map(data.items, (item) => KubernetesHorizontalPodAutoScalerConverter.apiToModel(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve HorizontalPodAutoScalers', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + // /** + // * CREATE + // */ + // async createAsync(horizontalPodAutoScaler) { + // try { + // const params = {}; + // const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); + // const namespace = payload.metadata.namespace; + // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; + // return data; + // } catch (err) { + // throw new PortainerError('Unable to create horizontalPodAutoScaler', err); + // } + // } + + // create(horizontalPodAutoScaler) { + // return this.$async(this.createAsync, horizontalPodAutoScaler); + // } + + // /** + // * PATCH + // */ + // async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = newHorizontalPodAutoScaler.Name; + // const namespace = newHorizontalPodAutoScaler.Namespace; + // const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + // if (!payload.length) { + // return; + // } + // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; + // return data; + // } catch (err) { + // throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); + // } + // } + + // patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + // return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + // } + + // /** + // * DELETE + // */ + // async deleteAsync(horizontalPodAutoScaler) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = horizontalPodAutoScaler.Name; + // const namespace = horizontalPodAutoScaler.Namespace; + // await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; + // } catch (err) { + // throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); + // } + // } + + // delete(horizontalPodAutoScaler) { + // return this.$async(this.deleteAsync, horizontalPodAutoScaler); + // } + + // /** + // * ROLLBACK + // */ + // async rollbackAsync(namespace, name, payload) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = name; + // await this.KubernetesHorizontalPodAutoScalers(namespace).rollback(params, payload).$promise; + // } catch (err) { + // throw new PortainerError('Unable to rollback horizontalPodAutoScaler', err); + // } + // } + + // rollback(namespace, name, payload) { + // return this.$async(this.rollbackAsync, namespace, name, payload); + // } +} + +export default KubernetesHorizontalPodAutoScalerService; +angular.module('portainer.kubernetes').service('KubernetesHorizontalPodAutoScalerService', KubernetesHorizontalPodAutoScalerService); diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js new file mode 100644 index 000000000..72af5bbf2 --- /dev/null +++ b/app/kubernetes/models/application/formValues.js @@ -0,0 +1,118 @@ +import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationDataAccessPolicies } from './models'; + +/** + * KubernetesApplicationFormValues Model + */ +const _KubernetesApplicationFormValues = Object.freeze({ + ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation) + ResourcePool: {}, + Name: '', + StackName: '', + ApplicationOwner: '', + Image: '', + ReplicaCount: 1, + Note: '', + EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list + PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list + PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list + MemoryLimit: 0, + CpuLimit: 0, + DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED, + PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, + DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, + Configurations: [], // KubernetesApplicationConfigurationFormValue list +}); + +export class KubernetesApplicationFormValues { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues))); + } +} + +export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({ + ENVIRONMENT: 1, + FILESYSTEM: 2, +}); + +/** + * KubernetesApplicationConfigurationFormValueOverridenKey Model + */ +const _KubernetesApplicationConfigurationFormValueOverridenKey = Object.freeze({ + Key: '', + Path: '', + Type: KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT, +}); + +export class KubernetesApplicationConfigurationFormValueOverridenKey { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValueOverridenKey))); + } +} + +/** + * KubernetesApplicationConfigurationFormValue Model + */ +const _KubernetesApplicationConfigurationFormValue = Object.freeze({ + SelectedConfiguration: undefined, + Overriden: false, + OverridenKeys: [], // KubernetesApplicationConfigurationFormValueOverridenKey list +}); + +export class KubernetesApplicationConfigurationFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValue))); + } +} + +/** + * KubernetesApplicationEnvironmentVariableFormValue Model + */ +const _KubernetesApplicationEnvironmentVariableFormValue = Object.freeze({ + Name: '', + Value: '', + IsSecret: false, + NeedsDeletion: false, + IsNew: true, +}); + +export class KubernetesApplicationEnvironmentVariableFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvironmentVariableFormValue))); + } +} + +/** + * KubernetesApplicationPersistedFolderFormValue Model + */ +const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({ + PersistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit) + NeedsDeletion: false, + ContainerPath: '', + Size: '', + SizeUnit: 'GB', + StorageClass: {}, +}); + +export class KubernetesApplicationPersistedFolderFormValue { + constructor(storageClass) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolderFormValue))); + this.StorageClass = storageClass; + } +} + +/** + * KubernetesApplicationPublishedPortFormValue Model + */ +const _KubernetesApplicationPublishedPortFormValue = Object.freeze({ + ContainerPort: '', + NodePort: '', + LoadBalancerPort: '', + LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort + Protocol: 'TCP', +}); + +export class KubernetesApplicationPublishedPortFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue))); + } +} diff --git a/app/kubernetes/models/application/models.js b/app/kubernetes/models/application/models.js new file mode 100644 index 000000000..3589a5ceb --- /dev/null +++ b/app/kubernetes/models/application/models.js @@ -0,0 +1,114 @@ +export const KubernetesApplicationDeploymentTypes = Object.freeze({ + REPLICATED: 1, + GLOBAL: 2, +}); + +export const KubernetesApplicationDataAccessPolicies = Object.freeze({ + SHARED: 1, + ISOLATED: 2, +}); + +export const KubernetesApplicationTypes = Object.freeze({ + DEPLOYMENT: 1, + DAEMONSET: 2, + STATEFULSET: 3, +}); + +export const KubernetesApplicationTypeStrings = Object.freeze({ + DEPLOYMENT: 'Deployment', + DAEMONSET: 'DaemonSet', + STATEFULSET: 'StatefulSet', +}); + +export const KubernetesApplicationPublishingTypes = Object.freeze({ + INTERNAL: 1, + CLUSTER: 2, + LOAD_BALANCER: 3, +}); + +export const KubernetesApplicationQuotaDefaults = { + CpuLimit: 0.1, + MemoryLimit: 64, // MB +}; + +export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; + +export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name'; + +export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; + +export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note'; + +/** + * KubernetesApplication Model (Composite) + */ +const _KubernetesApplication = Object.freeze({ + Id: '', + Name: '', + StackName: '', + ApplicationOwner: '', + ApplicationName: '', + ResourcePool: '', + Image: '', + CreationDate: 0, + Pods: [], + Limits: {}, + ServiceType: '', + ServiceId: '', + ServiceName: '', + HeadlessServiceName: undefined, // only used for StatefulSet + LoadBalancerIPAddress: undefined, // only filled when bound service is LoadBalancer and state is available + PublishedPorts: [], + Volumes: [], + Env: [], + PersistedFolders: [], // KubernetesApplicationPersistedFolder list + ConfigurationVolumes: [], // KubernetesApplicationConfigurationVolume list + DeploymentType: 'Unknown', + DataAccessPolicy: 'Unknown', + ApplicationType: 'Unknown', + RunningPodsCount: 0, + TotalPodsCount: 0, + Yaml: '', + Note: '', + Revisions: undefined, + CurrentRevision: undefined, + Raw: undefined, // only filled when inspecting app details / create / edit view (never filled in multiple-apps views) + AutoScaler: undefined, // only filled if the application has an HorizontalPodAutoScaler bound to it +}); + +export class KubernetesApplication { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplication))); + } +} + +/** + * KubernetesApplicationPersistedFolder Model + */ +const _KubernetesApplicationPersistedFolder = Object.freeze({ + MountPath: '', + PersistentVolumeClaimName: '', + HostPath: '', +}); + +export class KubernetesApplicationPersistedFolder { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolder))); + } +} + +/** + * KubernetesApplicationConfigurationVolume Model + */ +const _KubernetesApplicationConfigurationVolume = Object.freeze({ + fileMountPath: '', + rootMountPath: '', + configurationKey: '', + configurationName: '', +}); + +export class KubernetesApplicationConfigurationVolume { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationVolume))); + } +} diff --git a/app/kubernetes/models/application/payloads.js b/app/kubernetes/models/application/payloads.js new file mode 100644 index 000000000..4671e9bf9 --- /dev/null +++ b/app/kubernetes/models/application/payloads.js @@ -0,0 +1,142 @@ +/////////////////////////// VOLUME MOUNT /////////////////////////////// +/** + * KubernetesApplicationVolumeMount Model + */ +const _KubernetesApplicationVolumeMount = Object.freeze({ + name: '', + mountPath: '', + readOnly: false, +}); + +export class KubernetesApplicationVolumeMountPayload { + constructor(readOnly) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeMount))); + if (readOnly) { + this.readOnly = true; + } else { + delete this.readOnly; + } + } +} + +///////////////////////////////// PVC ///////////////////////////////// +/** + * KubernetesApplicationVolumePersistentPayload Model + */ +const _KubernetesApplicationVolumePersistentPayload = Object.freeze({ + name: '', + persistentVolumeClaim: { + claimName: '', + }, +}); + +export class KubernetesApplicationVolumePersistentPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumePersistentPayload))); + } +} + +/////////////////////////////// CONFIG AS VOLUME //////////////////////// +/** + * KubernetesApplicationVolumeConfigMapPayload Model + */ +const _KubernetesApplicationVolumeConfigMapPayload = Object.freeze({ + name: '', + configMap: { + name: '', + items: [], // KubernetesApplicationVolumeEntryPayload + }, +}); + +export class KubernetesApplicationVolumeConfigMapPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeConfigMapPayload))); + } +} + +//////////////////// SECRET AS VOLUME ///////////////////////////////////// +/** + * KubernetesApplicationVolumeSecretPayload Model + */ +const _KubernetesApplicationVolumeSecretPayload = Object.freeze({ + name: '', + secret: { + secretName: '', + items: [], // KubernetesApplicationVolumeEntryPayload + }, +}); + +export class KubernetesApplicationVolumeSecretPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeSecretPayload))); + } +} + +/** + * KubernetesApplicationVolumeEntryPayload Model + */ +const _KubernetesApplicationVolumeEntryPayload = Object.freeze({ + key: '', + path: '', +}); + +export class KubernetesApplicationVolumeEntryPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeEntryPayload))); + } +} + +//////////////////////////// ENV ENTRY ////////////////////////////// +/** + * KubernetesApplicationEnvPayload Model + */ +const _KubernetesApplicationEnvPayload = Object.freeze({ + name: '', + value: '', +}); + +export class KubernetesApplicationEnvPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvPayload))); + } +} + +///////////////////////// CONFIG AS ENV //////////////////////////////// +/** + * KubernetesApplicationEnvConfigMapPayload Model + */ +const _KubernetesApplicationEnvConfigMapPayload = Object.freeze({ + name: '', + valueFrom: { + configMapKeyRef: { + name: '', + key: '', + }, + }, +}); + +export class KubernetesApplicationEnvConfigMapPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvConfigMapPayload))); + } +} + +//////////////////////// SECRET AS ENV ////////////////////////////////// +/** + * KubernetesApplicationEnvSecretPayload Model + */ +const _KubernetesApplicationEnvSecretPayload = Object.freeze({ + name: '', + valueFrom: { + secretKeyRef: { + name: '', + key: '', + }, + }, +}); + +export class KubernetesApplicationEnvSecretPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvSecretPayload))); + } +} diff --git a/app/kubernetes/models/common/params.js b/app/kubernetes/models/common/params.js new file mode 100644 index 000000000..fd37b8764 --- /dev/null +++ b/app/kubernetes/models/common/params.js @@ -0,0 +1,11 @@ +/** + * Generic params + */ +const _KubernetesCommonParams = Object.freeze({ + id: '', +}); +export class KubernetesCommonParams { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams))); + } +} diff --git a/app/kubernetes/models/common/payloads.js b/app/kubernetes/models/common/payloads.js new file mode 100644 index 000000000..28909d338 --- /dev/null +++ b/app/kubernetes/models/common/payloads.js @@ -0,0 +1,15 @@ +/** + * Generic metadata payload + */ +const _KubernetesCommonMetadataPayload = Object.freeze({ + uid: '', + name: '', + namespace: '', + labels: {}, + annotations: {}, +}); +export class KubernetesCommonMetadataPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonMetadataPayload))); + } +} diff --git a/app/kubernetes/models/config-map/models.js b/app/kubernetes/models/config-map/models.js new file mode 100644 index 000000000..b0463042c --- /dev/null +++ b/app/kubernetes/models/config-map/models.js @@ -0,0 +1,21 @@ +export const KubernetesPortainerConfigMapNamespace = 'portainer'; +export const KubernetesPortainerConfigMapConfigName = 'portainer-config'; +export const KubernetesPortainerConfigMapAccessKey = 'NamespaceAccessPolicies'; + +/** + * ConfigMap Model + */ +const _KubernetesConfigMap = Object.freeze({ + Id: 0, + Name: '', + Namespace: '', + Yaml: '', + ConfigurationOwner: '', + Data: {}, +}); + +export class KubernetesConfigMap { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMap))); + } +} diff --git a/app/kubernetes/models/config-map/payloads.js b/app/kubernetes/models/config-map/payloads.js new file mode 100644 index 000000000..14e8fe708 --- /dev/null +++ b/app/kubernetes/models/config-map/payloads.js @@ -0,0 +1,27 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * Payload for CREATE + */ +const _KubernetesConfigMapCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + data: {}, +}); +export class KubernetesConfigMapCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMapCreatePayload))); + } +} + +/** + * Payload for UPDATE + */ +const _KubernetesConfigMapUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + data: {}, +}); +export class KubernetesConfigMapUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMapUpdatePayload))); + } +} diff --git a/app/kubernetes/models/configuration/formvalues.js b/app/kubernetes/models/configuration/formvalues.js new file mode 100644 index 000000000..859542651 --- /dev/null +++ b/app/kubernetes/models/configuration/formvalues.js @@ -0,0 +1,35 @@ +import { KubernetesConfigurationTypes } from './models'; + +/** + * KubernetesConfigurationFormValues Model + */ +const _KubernetesConfigurationFormValues = Object.freeze({ + Id: '', + ResourcePool: '', + Name: '', + ConfigurationOwner: '', + Type: KubernetesConfigurationTypes.CONFIGMAP, + Data: [], + DataYaml: '', + IsSimple: true, +}); + +export class KubernetesConfigurationFormValues { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValues))); + } +} + +/** + * KubernetesConfigurationEntry Model + */ +const _KubernetesConfigurationFormValuesDataEntry = Object.freeze({ + Key: '', + Value: '', +}); + +export class KubernetesConfigurationFormValuesDataEntry { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValuesDataEntry))); + } +} diff --git a/app/kubernetes/models/configuration/models.js b/app/kubernetes/models/configuration/models.js new file mode 100644 index 000000000..08c5ae90a --- /dev/null +++ b/app/kubernetes/models/configuration/models.js @@ -0,0 +1,27 @@ +export const KubernetesPortainerConfigurationOwnerLabel = 'io.portainer.kubernetes.configuration.owner'; + +/** + * Configuration Model (Composite) + */ +const _KubernetesConfiguration = Object.freeze({ + Id: 0, + Name: '', + Type: '', + Namespace: '', + CreationDate: '', + ConfigurationOwner: '', + Used: false, + Applications: [], + Data: {}, +}); + +export class KubernetesConfiguration { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfiguration))); + } +} + +export const KubernetesConfigurationTypes = Object.freeze({ + CONFIGMAP: 1, + SECRET: 2, +}); diff --git a/app/kubernetes/models/daemon-set/models.js b/app/kubernetes/models/daemon-set/models.js new file mode 100644 index 000000000..ba0e4d6cd --- /dev/null +++ b/app/kubernetes/models/daemon-set/models.js @@ -0,0 +1,24 @@ +/** + * KubernetesDaemonSet Model + */ +const _KubernetesDaemonSet = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + Image: '', + Env: [], + CpuLimit: 0, + MemoryLimit: 0, + VoluemMounts: [], + Volumes: [], + Secret: undefined, + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesDaemonSet { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDaemonSet))); + } +} diff --git a/app/kubernetes/models/daemon-set/payloads.js b/app/kubernetes/models/daemon-set/payloads.js new file mode 100644 index 000000000..737fbc122 --- /dev/null +++ b/app/kubernetes/models/daemon-set/payloads.js @@ -0,0 +1,50 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesDaemonSetCreatePayload Model + */ +const _KubernetesDaemonSetCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + selector: { + matchLabels: { + app: '', + }, + }, + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { + maxUnavailable: 1, + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesDaemonSetCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDaemonSetCreatePayload))); + } +} diff --git a/app/kubernetes/models/deploy.js b/app/kubernetes/models/deploy.js new file mode 100644 index 000000000..d9e0d8363 --- /dev/null +++ b/app/kubernetes/models/deploy.js @@ -0,0 +1,4 @@ +export const KubernetesDeployManifestTypes = Object.freeze({ + KUBERNETES: 1, + COMPOSE: 2, +}); diff --git a/app/kubernetes/models/deployment/models.js b/app/kubernetes/models/deployment/models.js new file mode 100644 index 000000000..05161e929 --- /dev/null +++ b/app/kubernetes/models/deployment/models.js @@ -0,0 +1,25 @@ +/** + * KubernetesDeployment Model + */ +const _KubernetesDeployment = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + ReplicaCount: 0, + Image: '', + Env: [], + CpuLimit: 0, + MemoryLimit: 0, + VolumeMounts: [], + Volumes: [], + Secret: undefined, + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesDeployment { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDeployment))); + } +} diff --git a/app/kubernetes/models/deployment/payloads.js b/app/kubernetes/models/deployment/payloads.js new file mode 100644 index 000000000..89047b08f --- /dev/null +++ b/app/kubernetes/models/deployment/payloads.js @@ -0,0 +1,51 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesDeploymentCreatePayload Model + */ +const _KubernetesDeploymentCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + selector: { + matchLabels: { + app: '', + }, + }, + strategy: { + type: 'RollingUpdate', + rollingUpdate: { + maxSurge: 0, + maxUnavailable: '100%', + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesDeploymentCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDeploymentCreatePayload))); + } +} diff --git a/app/kubernetes/models/event/models.js b/app/kubernetes/models/event/models.js new file mode 100644 index 000000000..f79c8e43b --- /dev/null +++ b/app/kubernetes/models/event/models.js @@ -0,0 +1,16 @@ +/** + * KubernetesEvent Model + */ +const _KubernetesEvent = Object.freeze({ + Id: '', + Date: 0, + Type: '', + Message: '', + Involved: {}, +}); + +export class KubernetesEvent { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesEvent))); + } +} diff --git a/app/kubernetes/models/history/models.js b/app/kubernetes/models/history/models.js new file mode 100644 index 000000000..8a8cad6a7 --- /dev/null +++ b/app/kubernetes/models/history/models.js @@ -0,0 +1,32 @@ +export const KubernetesSystem_DefaultDeploymentUniqueLabelKey = 'pod-template-hash'; +export const KubernetesSystem_RevisionAnnotation = 'deployment.kubernetes.io/revision'; +export const KubernetesSystem_RevisionHistoryAnnotation = 'deployment.kubernetes.io/revision-history'; +export const KubernetesSystem_ChangeCauseAnnotation = 'kubernetes.io/change-cause'; +export const KubernetesSystem_DesiredReplicasAnnotation = 'deployment.kubernetes.io/desired-replicas'; +export const KubernetesSystem_MaxReplicasAnnotation = 'deployment.kubernetes.io/max-replicas'; + +// annotationsToSkip lists the annotations that should be preserved from the deployment and not +// copied from the replicaset when rolling a deployment back +// var annotationsToSkip = map[string]bool{ +// corev1.LastAppliedConfigAnnotation: true, +// deploymentutil.RevisionAnnotation: true, +// deploymentutil.RevisionHistoryAnnotation: true, +// deploymentutil.DesiredReplicasAnnotation: true, +// deploymentutil.MaxReplicasAnnotation: true, +// appsv1.DeprecatedRollbackTo: true, +// } + +// LastAppliedConfigAnnotation is the annotation used to store the previous +// configuration of a resource for use in a three way diff by UpdateApplyAnnotation. +const LastAppliedConfigAnnotation = 'kubectl.kubernetes.io/last-applied-configuration'; + +const DeprecatedRollbackTo = 'deprecated.deployment.rollback.to'; + +export const KubernetesSystem_AnnotationsToSkip = { + [LastAppliedConfigAnnotation]: true, + [KubernetesSystem_RevisionAnnotation]: true, + [KubernetesSystem_RevisionHistoryAnnotation]: true, + [KubernetesSystem_DesiredReplicasAnnotation]: true, + [KubernetesSystem_MaxReplicasAnnotation]: true, + [DeprecatedRollbackTo]: true, +}; diff --git a/app/kubernetes/models/namespace/models.js b/app/kubernetes/models/namespace/models.js new file mode 100644 index 000000000..d4038141a --- /dev/null +++ b/app/kubernetes/models/namespace/models.js @@ -0,0 +1,18 @@ +/** + * KubernetesNamespace Model + */ +const _KubernetesNamespace = Object.freeze({ + Id: '', + Name: '', + CreationDate: '', + Status: '', + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', +}); + +export class KubernetesNamespace { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespace))); + } +} diff --git a/app/kubernetes/models/namespace/payloads.js b/app/kubernetes/models/namespace/payloads.js new file mode 100644 index 000000000..eb0d42fc5 --- /dev/null +++ b/app/kubernetes/models/namespace/payloads.js @@ -0,0 +1,14 @@ +import { KubernetesCommonMetadataPayload } from '../common/payloads'; + +/** + * KubernetesNamespaceCreatePayload Model + */ +const _KubernetesNamespaceCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), +}); + +export class KubernetesNamespaceCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespaceCreatePayload))); + } +} diff --git a/app/kubernetes/models/node/models.js b/app/kubernetes/models/node/models.js new file mode 100644 index 000000000..aeee2789b --- /dev/null +++ b/app/kubernetes/models/node/models.js @@ -0,0 +1,40 @@ +/** + * KubernetesNode Model + */ +const _KubernetesNode = Object.freeze({ + Id: '', + Name: '', + Role: '', + Status: '', + CPU: 0, + Memory: '', + Version: '', + IPAddress: '', +}); + +export class KubernetesNode { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNode))); + } +} + +/** + * KubernetesNodeDetails Model + */ +const _KubernetesNodeDetails = Object.freeze({ + CreationDate: '', + OS: { + Architecture: '', + Platform: '', + Image: '', + }, + Conditions: [], + Yaml: '', +}); + +export class KubernetesNodeDetails { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNode))); + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeDetails))); + } +} diff --git a/app/kubernetes/models/pod/models.js b/app/kubernetes/models/pod/models.js new file mode 100644 index 000000000..3989913c6 --- /dev/null +++ b/app/kubernetes/models/pod/models.js @@ -0,0 +1,21 @@ +/** + * KubernetesPod Model + */ +const _KubernetesPod = Object.freeze({ + Id: '', + Name: '', + Namespace: '', + Images: [], + Status: '', + Restarts: 0, + Node: '', + CreationDate: '', + Containers: [], + Labels: [], +}); + +export class KubernetesPod { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod))); + } +} diff --git a/app/kubernetes/models/port/models.js b/app/kubernetes/models/port/models.js new file mode 100644 index 000000000..1409a1059 --- /dev/null +++ b/app/kubernetes/models/port/models.js @@ -0,0 +1,33 @@ +/** + * PortMappingPort Model + */ +const _KubernetesPortMappingPort = Object.freeze({ + Port: 0, + TargetPort: 0, + Protocol: '', +}); + +export class KubernetesPortMappingPort { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPortMappingPort))); + } +} + +/** + * PortMapping Model + */ +const _KubernetesPortMapping = Object.freeze({ + Expanded: false, + Highlighted: false, + ResourcePool: '', + ServiceType: '', + ApplicationOwner: '', + LoadBalancerIPAddress: '', + Ports: [], +}); + +export class KubernetesPortMapping { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPortMapping))); + } +} diff --git a/app/kubernetes/models/resource-pool/models.js b/app/kubernetes/models/resource-pool/models.js new file mode 100644 index 000000000..57fa71df9 --- /dev/null +++ b/app/kubernetes/models/resource-pool/models.js @@ -0,0 +1,20 @@ +export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes.resourcepool.name'; + +export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner'; + +/** + * KubernetesResourcePool Model (Composite) + * ResourcePool is a composite model that includes + * A Namespace and a Quota + */ +const _KubernetesResourcePool = Object.freeze({ + Namespace: {}, // KubernetesNamespace + Quota: undefined, // KubernetesResourceQuota + Yaml: '', +}); + +export class KubernetesResourcePool { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourcePool))); + } +} diff --git a/app/kubernetes/models/resource-quota/models.js b/app/kubernetes/models/resource-quota/models.js new file mode 100644 index 000000000..63190b760 --- /dev/null +++ b/app/kubernetes/models/resource-quota/models.js @@ -0,0 +1,34 @@ +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; + +export const KubernetesPortainerResourceQuotaPrefix = 'portainer-rq-'; + +export const KubernetesResourceQuotaDefaults = { + CpuLimit: 0, + MemoryLimit: 0, +}; + +/** + * KubernetesResourceQuota Model + */ +const _KubernetesResourceQuota = Object.freeze({ + Id: '', + Namespace: '', + Name: '', + CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit, + CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit, + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', +}); + +export class KubernetesResourceQuota { + constructor(namespace) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuota))); + if (namespace) { + this.Name = KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace); + this.Namespace = namespace; + } + } +} diff --git a/app/kubernetes/models/resource-quota/payloads.js b/app/kubernetes/models/resource-quota/payloads.js new file mode 100644 index 000000000..9a04dbf6c --- /dev/null +++ b/app/kubernetes/models/resource-quota/payloads.js @@ -0,0 +1,43 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesResourceQuotaCreatePayload Model + */ +const _KubernetesResourceQuotaCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + hard: { + 'requests.cpu': 0, + 'requests.memory': 0, + 'limits.cpu': 0, + 'limits.memory': 0, + }, + }, +}); + +export class KubernetesResourceQuotaCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaCreatePayload))); + } +} + +/** + * KubernetesResourceQuotaUpdatePayload Model + */ +const _KubernetesResourceQuotaUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + hard: { + 'requests.cpu': 0, + 'requests.memory': 0, + 'limits.cpu': 0, + 'limits.memory': 0, + }, + }, +}); + +export class KubernetesResourceQuotaUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaUpdatePayload))); + } +} diff --git a/app/kubernetes/models/resource-reservation/models.js b/app/kubernetes/models/resource-reservation/models.js new file mode 100644 index 000000000..ad13f6c70 --- /dev/null +++ b/app/kubernetes/models/resource-reservation/models.js @@ -0,0 +1,13 @@ +/** + * KubernetesResourceReservation Model + */ +const _KubernetesResourceReservation = Object.freeze({ + Memory: 0, + CPU: 0, +}); + +export class KubernetesResourceReservation { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceReservation))); + } +} diff --git a/app/kubernetes/models/secret/models.js b/app/kubernetes/models/secret/models.js new file mode 100644 index 000000000..ae47b8b2e --- /dev/null +++ b/app/kubernetes/models/secret/models.js @@ -0,0 +1,18 @@ +/** + * KubernetesApplicationSecret Model + */ +const _KubernetesApplicationSecret = Object.freeze({ + Id: 0, + Name: '', + Namespace: '', + CreationDate: '', + ConfigurationOwner: '', + Yaml: '', + Data: {}, +}); + +export class KubernetesApplicationSecret { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationSecret))); + } +} diff --git a/app/kubernetes/models/secret/payloads.js b/app/kubernetes/models/secret/payloads.js new file mode 100644 index 000000000..2ce761d04 --- /dev/null +++ b/app/kubernetes/models/secret/payloads.js @@ -0,0 +1,31 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesSecretCreatePayload Model + */ +const _KubernetesSecretCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + type: 'Opaque', + data: {}, +}); + +export class KubernetesSecretCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesSecretCreatePayload))); + } +} + +/** + * KubernetesSecretUpdatePayload Model + */ +const _KubernetesSecretUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + type: 'Opaque', + data: {}, +}); + +export class KubernetesSecretUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesSecretUpdatePayload))); + } +} diff --git a/app/kubernetes/models/service/models.js b/app/kubernetes/models/service/models.js new file mode 100644 index 000000000..5c9abb61c --- /dev/null +++ b/app/kubernetes/models/service/models.js @@ -0,0 +1,45 @@ +export const KubernetesServiceHeadlessPrefix = 'headless-'; +export const KubernetesServiceHeadlessClusterIP = 'None'; +export const KubernetesServiceTypes = Object.freeze({ + LOAD_BALANCER: 'LoadBalancer', + NODE_PORT: 'NodePort', +}); + +/** + * KubernetesService Model + */ +const _KubernetesService = Object.freeze({ + Headless: false, + Namespace: '', + Name: '', + StackName: '', + Ports: [], + Type: '', + ClusterIP: '', + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesService { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesService))); + } +} + +/** + * KubernetesServicePort Model + */ +const _KubernetesServicePort = Object.freeze({ + name: '', + port: 0, + targetPort: 0, + protocol: '', + nodePort: 0, +}); + +export class KubernetesServicePort { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesServicePort))); + } +} diff --git a/app/kubernetes/models/service/payloads.js b/app/kubernetes/models/service/payloads.js new file mode 100644 index 000000000..a5a3c2d5e --- /dev/null +++ b/app/kubernetes/models/service/payloads.js @@ -0,0 +1,22 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesServiceCreatePayload Model + */ +const _KubernetesServiceCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + ports: [], + selector: { + app: '', + }, + type: '', + clusterIP: '', + }, +}); + +export class KubernetesServiceCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesServiceCreatePayload))); + } +} diff --git a/app/kubernetes/models/stack/models.js b/app/kubernetes/models/stack/models.js new file mode 100644 index 000000000..1c9c27494 --- /dev/null +++ b/app/kubernetes/models/stack/models.js @@ -0,0 +1,14 @@ +/** + * Stack Model + */ +const _KubernetesStack = Object.freeze({ + Name: '', + ResourcePool: '', + Applications: [], +}); + +export class KubernetesStack { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStack))); + } +} diff --git a/app/kubernetes/models/stateful-set/models.js b/app/kubernetes/models/stateful-set/models.js new file mode 100644 index 000000000..7520ec116 --- /dev/null +++ b/app/kubernetes/models/stateful-set/models.js @@ -0,0 +1,27 @@ +/** + * KubernetesStatefulSet Model + */ +const _KubernetesStatefulSet = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + ReplicaCount: 0, + Image: '', + Env: [], + CpuLimit: '', + MemoryLimit: '', + VolumeMounts: [], + Volumes: [], + Secret: undefined, + VolumeClaims: [], + ServiceName: '', + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesStatefulSet { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStatefulSet))); + } +} diff --git a/app/kubernetes/models/stateful-set/payloads.js b/app/kubernetes/models/stateful-set/payloads.js new file mode 100644 index 000000000..b5bd40329 --- /dev/null +++ b/app/kubernetes/models/stateful-set/payloads.js @@ -0,0 +1,52 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesStatefulSetCreatePayload Model + */ +const _KubernetesStatefulSetCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + serviceName: '', + selector: { + matchLabels: { + app: '', + }, + }, + volumeClaimTemplates: [], + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { + partition: 0, + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesStatefulSetCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStatefulSetCreatePayload))); + } +} diff --git a/app/kubernetes/models/storage-class/models.js b/app/kubernetes/models/storage-class/models.js new file mode 100644 index 000000000..6ede8946a --- /dev/null +++ b/app/kubernetes/models/storage-class/models.js @@ -0,0 +1,33 @@ +/** + * KubernetesStorageClassAccessPolicies Model + */ +const _KubernetesStorageClassAccessPolicies = Object.freeze([ + { + Name: 'RWO', + Description: 'Allow read-write from a single pod only (RWO)', + selected: true, + }, + { + Name: 'RWX', + Description: 'Allow read-write access from one or more pods concurrently (RWX)', + selected: false, + }, +]); + +export function KubernetesStorageClassAccessPolicies() { + return JSON.parse(JSON.stringify(_KubernetesStorageClassAccessPolicies)); +} + +/** + * KubernetesStorageClass Model + */ +const _KubernetesStorageClass = Object.freeze({ + Name: '', + AccessModes: [], +}); + +export class KubernetesStorageClass { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStorageClass))); + } +} diff --git a/app/kubernetes/models/volume/models.js b/app/kubernetes/models/volume/models.js new file mode 100644 index 000000000..aaaee4054 --- /dev/null +++ b/app/kubernetes/models/volume/models.js @@ -0,0 +1,39 @@ +import uuidv4 from 'uuid/v4'; +/** + * KubernetesPersistentVolumeClaim Model + */ +const _KubernetesPersistentVolumeClaim = Object.freeze({ + Id: '', + Name: '', + PreviousName: '', + Namespace: '', + Storage: 0, + StorageClass: {}, // KubernetesStorageClass + CreationDate: '', + ApplicationOwner: '', + ApplicationName: '', + MountPath: '', // used for Application creation from ApplicationFormValues | not used from API conversion + Yaml: '', +}); + +export class KubernetesPersistentVolumeClaim { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPersistentVolumeClaim))); + this.Name = uuidv4(); + } +} + +/** + * KubernetesVolume Model (Composite) + */ +const _KubernetesVolume = Object.freeze({ + ResourcePool: {}, // KubernetesResourcePool + PersistentVolumeClaim: {}, // KubernetesPersistentVolumeClaim + Applications: [], // KubernetesApplication +}); + +export class KubernetesVolume { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesVolume))); + } +} diff --git a/app/kubernetes/models/volume/payloads.js b/app/kubernetes/models/volume/payloads.js new file mode 100644 index 000000000..3fbd14936 --- /dev/null +++ b/app/kubernetes/models/volume/payloads.js @@ -0,0 +1,23 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesPersistentVolumClaimCreatePayload Model + */ +const _KubernetesPersistentVolumClaimCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + accessModes: ['ReadWriteOnce'], + resources: { + requests: { + storage: '', + }, + }, + storageClassName: '', + }, +}); + +export class KubernetesPersistentVolumClaimCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPersistentVolumClaimCreatePayload))); + } +} diff --git a/app/kubernetes/rest/configMap.js b/app/kubernetes/rest/configMap.js new file mode 100644 index 000000000..62ad844dd --- /dev/null +++ b/app/kubernetes/rest/configMap.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesConfigMaps', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesConfigMapsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/configmaps/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/controllerRevision.js b/app/kubernetes/rest/controllerRevision.js new file mode 100644 index 000000000..fb8892c32 --- /dev/null +++ b/app/kubernetes/rest/controllerRevision.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesControllerRevisions', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesControllerRevisionsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/controllerrevisions/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/daemonSet.js b/app/kubernetes/rest/daemonSet.js new file mode 100644 index 000000000..f2cfa025d --- /dev/null +++ b/app/kubernetes/rest/daemonSet.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesDaemonSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesDaemonSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/daemonsets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/strategic-merge-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/deployment.js b/app/kubernetes/rest/deployment.js new file mode 100644 index 000000000..44b2eb662 --- /dev/null +++ b/app/kubernetes/rest/deployment.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesDeployments', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesDeploymentsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/deployments/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/event.js b/app/kubernetes/rest/event.js new file mode 100644 index 000000000..54f9b07a5 --- /dev/null +++ b/app/kubernetes/rest/event.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesEvents', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesEventsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/events/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/health.js b/app/kubernetes/rest/health.js new file mode 100644 index 000000000..012798057 --- /dev/null +++ b/app/kubernetes/rest/health.js @@ -0,0 +1,17 @@ +angular.module('portainer.kubernetes').factory('KubernetesHealth', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesHealthFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/healthz', + { + endpointId: EndpointProvider.endpointID, + }, + { + ping: { method: 'GET', timeout: 15000 }, + } + ); + }, +]); diff --git a/app/kubernetes/rest/namespace.js b/app/kubernetes/rest/namespace.js new file mode 100644 index 000000000..843893d58 --- /dev/null +++ b/app/kubernetes/rest/namespace.js @@ -0,0 +1,42 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesNamespaces', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesNamespacesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1/namespaces/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + status: { + method: 'GET', + params: { action: 'status' }, + ignoreLoadingBar: true, + }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/node.js b/app/kubernetes/rest/node.js new file mode 100644 index 000000000..5e27a53eb --- /dev/null +++ b/app/kubernetes/rest/node.js @@ -0,0 +1,37 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesNodes', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesNodesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1/nodes/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/persistentVolumeClaim.js b/app/kubernetes/rest/persistentVolumeClaim.js new file mode 100644 index 000000000..25dd65177 --- /dev/null +++ b/app/kubernetes/rest/persistentVolumeClaim.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesPersistentVolumeClaims', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesPersistentVolumeClaimsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/persistentvolumeclaims/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/pod.js b/app/kubernetes/rest/pod.js new file mode 100644 index 000000000..2726eadd3 --- /dev/null +++ b/app/kubernetes/rest/pod.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; +import { logsHandler } from 'Docker/rest/response/handlers'; + +angular.module('portainer.kubernetes').factory('KubernetesPods', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesPodsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/pods/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + logs: { + method: 'GET', + params: { action: 'log' }, + transformResponse: logsHandler, + }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/replicaSet.js b/app/kubernetes/rest/replicaSet.js new file mode 100644 index 000000000..45dbf6c2c --- /dev/null +++ b/app/kubernetes/rest/replicaSet.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesReplicaSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesReplicaSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/replicasets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/resourceQuota.js b/app/kubernetes/rest/resourceQuota.js new file mode 100644 index 000000000..c1eae1036 --- /dev/null +++ b/app/kubernetes/rest/resourceQuota.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesResourceQuotasFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/resourcequotas/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/response/transform.js b/app/kubernetes/rest/response/transform.js new file mode 100644 index 000000000..d4a0db053 --- /dev/null +++ b/app/kubernetes/rest/response/transform.js @@ -0,0 +1,7 @@ +// Returns the raw response without JSON parsing +export function rawResponse(data) { + const response = { + data: data, + }; + return response; +} diff --git a/app/kubernetes/rest/secret.js b/app/kubernetes/rest/secret.js new file mode 100644 index 000000000..1767871aa --- /dev/null +++ b/app/kubernetes/rest/secret.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesSecrets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesSecretsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/secrets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/service.js b/app/kubernetes/rest/service.js new file mode 100644 index 000000000..c483f13f2 --- /dev/null +++ b/app/kubernetes/rest/service.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesServices', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesServicesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/services/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/statefulSet.js b/app/kubernetes/rest/statefulSet.js new file mode 100644 index 000000000..3fa962aa0 --- /dev/null +++ b/app/kubernetes/rest/statefulSet.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesStatefulSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesStatefulSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/statefulsets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/strategic-merge-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/storage.js b/app/kubernetes/rest/storage.js new file mode 100644 index 000000000..44e6406bf --- /dev/null +++ b/app/kubernetes/rest/storage.js @@ -0,0 +1,37 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesStorage', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesStorageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/storage.k8s.io/v1/storageclasses/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js new file mode 100644 index 000000000..ea99d26e1 --- /dev/null +++ b/app/kubernetes/services/applicationService.js @@ -0,0 +1,343 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback'; +import KubernetesApplicationConverter from 'Kubernetes/converters/application'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; +import { KubernetesApplication } from 'Kubernetes/models/application/models'; +import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; +import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; + +class KubernetesApplicationService { + /* @ngInject */ + constructor( + $async, + Authentication, + KubernetesDeploymentService, + KubernetesDaemonSetService, + KubernetesStatefulSetService, + KubernetesServiceService, + KubernetesSecretService, + KubernetesPersistentVolumeClaimService, + KubernetesNamespaceService, + KubernetesPodService, + KubernetesHistoryService, + KubernetesHorizontalPodAutoScalerService + ) { + this.$async = $async; + this.Authentication = Authentication; + this.KubernetesDeploymentService = KubernetesDeploymentService; + this.KubernetesDaemonSetService = KubernetesDaemonSetService; + this.KubernetesStatefulSetService = KubernetesStatefulSetService; + this.KubernetesServiceService = KubernetesServiceService; + this.KubernetesSecretService = KubernetesSecretService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesPodService = KubernetesPodService; + this.KubernetesHistoryService = KubernetesHistoryService; + this.KubernetesHorizontalPodAutoScalerService = KubernetesHorizontalPodAutoScalerService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.patchPartialAsync = this.patchPartialAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * UTILS + */ + _getApplicationApiService(app) { + let apiService; + if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) { + apiService = this.KubernetesDeploymentService; + } else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) { + apiService = this.KubernetesDaemonSetService; + } else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) { + apiService = this.KubernetesStatefulSetService; + } else { + throw new PortainerError('Unable to determine which association to use'); + } + return apiService; + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [deployment, daemonSet, statefulSet, pods, autoScalers] = await Promise.allSettled([ + this.KubernetesDeploymentService.get(namespace, name), + this.KubernetesDaemonSetService.get(namespace, name), + this.KubernetesStatefulSetService.get(namespace, name), + this.KubernetesPodService.get(namespace), + this.KubernetesHorizontalPodAutoScalerService.get(namespace), + ]); + + let rootItem; + let converterFunction; + if (deployment.status === 'fulfilled') { + rootItem = deployment; + converterFunction = KubernetesApplicationConverter.apiDeploymentToApplication; + } else if (daemonSet.status === 'fulfilled') { + rootItem = daemonSet; + converterFunction = KubernetesApplicationConverter.apiDaemonSetToApplication; + } else if (statefulSet.status === 'fulfilled') { + rootItem = statefulSet; + converterFunction = KubernetesApplicationConverter.apiStatefulSetToapplication; + } else { + throw new PortainerError('Unable to determine which association to use'); + } + + const services = await this.KubernetesServiceService.get(namespace); + const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); + const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; + + const application = converterFunction(rootItem.value.Raw, service.Raw); + application.Yaml = rootItem.value.Yaml; + application.Raw = rootItem.value.Raw; + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw); + + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + + await this.KubernetesHistoryService.get(application); + + if (service.Yaml) { + application.Yaml += '---\n' + service.Yaml; + } + if (scaler && scaler.Yaml) { + application.Yaml += '---\n' + scaler.Yaml; + } + return application; + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [deployments, daemonSets, statefulSets, services, pods] = await Promise.all([ + this.KubernetesDeploymentService.get(ns), + this.KubernetesDaemonSetService.get(ns), + this.KubernetesStatefulSetService.get(ns), + this.KubernetesServiceService.get(ns), + this.KubernetesPodService.get(ns), + ]); + const deploymentApplications = _.map(deployments, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiDeploymentToApplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + const daemonSetApplications = _.map(daemonSets, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiDaemonSetToApplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + const statefulSetApplications = _.map(statefulSets, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiStatefulSetToapplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + // TODO: review + // resource creation flow + // should we keep formValues > Resource_1 || Resource_2 + // or should we switch to formValues > Composite > Resource_1 || Resource_2 + async createAsync(formValues) { + try { + let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + + if (service) { + await this.KubernetesServiceService.create(service); + } + + const apiService = this._getApplicationApiService(app); + + if (app instanceof KubernetesStatefulSet) { + app.VolumeClaims = claims; + headlessService = await this.KubernetesServiceService.create(headlessService); + app.ServiceName = headlessService.metadata.name; + } else { + const claimPromises = _.map(claims, (item) => { + if (!item.PreviousName) { + return this.KubernetesPersistentVolumeClaimService.create(item); + } + }); + await Promise.all(_.without(claimPromises, undefined)); + } + + await apiService.create(app); + } catch (err) { + throw err; + } + } + + create(formValues) { + return this.$async(this.createAsync, formValues); + } + + /** + * PATCH + */ + // this function accepts KubernetesApplicationFormValues as parameters + async patchAsync(oldFormValues, newFormValues) { + try { + const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); + const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); + const oldApiService = this._getApplicationApiService(oldApp); + const newApiService = this._getApplicationApiService(newApp); + + if (oldApiService !== newApiService) { + await this.delete(oldApp); + if (oldService) { + await this.KubernetesServiceService.delete(oldService); + } + return await this.create(newFormValues); + } + + if (newApp instanceof KubernetesStatefulSet) { + await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); + } else { + const claimPromises = _.map(newClaims, (newClaim) => { + if (!newClaim.PreviousName) { + return this.KubernetesPersistentVolumeClaimService.create(newClaim); + } + const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); + return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); + }); + await Promise.all(claimPromises); + } + + await newApiService.patch(oldApp, newApp); + + if (oldService && newService) { + await this.KubernetesServiceService.patch(oldService, newService); + } else if (!oldService && newService) { + await this.KubernetesServiceService.create(newService); + } else if (oldService && !newService) { + await this.KubernetesServiceService.delete(oldService); + } + } catch (err) { + throw err; + } + } + + // this function accepts KubernetesApplication as parameters + async patchPartialAsync(oldApp, newApp) { + try { + const oldAppPayload = { + Name: oldApp.Name, + Namespace: oldApp.ResourcePool, + StackName: oldApp.StackName, + Note: oldApp.Note, + }; + const newAppPayload = { + Name: newApp.Name, + Namespace: newApp.ResourcePool, + StackName: newApp.StackName, + Note: newApp.Note, + }; + const apiService = this._getApplicationApiService(oldApp); + await apiService.patch(oldAppPayload, newAppPayload); + } catch (err) { + throw err; + } + } + + // accept either formValues or applications as parameters + // depending on partial value + // true = KubernetesApplication + // false = KubernetesApplicationFormValues + patch(oldValues, newValues, partial = false) { + if (partial) { + return this.$async(this.patchPartialAsync, oldValues, newValues); + } + return this.$async(this.patchAsync, oldValues, newValues); + } + + /** + * DELETE + */ + async deleteAsync(application) { + try { + const payload = { + Namespace: application.ResourcePool || application.Namespace, + Name: application.Name, + }; + const servicePayload = angular.copy(payload); + servicePayload.Name = application.Name; + + const apiService = this._getApplicationApiService(application); + await apiService.delete(payload); + + if (apiService === this.KubernetesStatefulSetService) { + const headlessServicePayload = angular.copy(payload); + headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName; + await this.KubernetesServiceService.delete(headlessServicePayload); + } + + if (application.ServiceType) { + await this.KubernetesServiceService.delete(servicePayload); + } + } catch (err) { + throw err; + } + } + + delete(application) { + return this.$async(this.deleteAsync, application); + } + + /** + * ROLLBACK + */ + async rollbackAsync(application, targetRevision) { + try { + const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision); + const apiService = this._getApplicationApiService(application); + await apiService.rollback(application.ResourcePool, application.Name, payload); + } catch (err) { + throw err; + } + } + + rollback(application, targetRevision) { + return this.$async(this.rollbackAsync, application, targetRevision); + } +} + +export default KubernetesApplicationService; +angular.module('portainer.kubernetes').service('KubernetesApplicationService', KubernetesApplicationService); diff --git a/app/kubernetes/services/configMapService.js b/app/kubernetes/services/configMapService.js new file mode 100644 index 000000000..247af7497 --- /dev/null +++ b/app/kubernetes/services/configMapService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesConfigMapService { + /* @ngInject */ + constructor($async, KubernetesConfigMaps) { + this.$async = $async; + this.KubernetesConfigMaps = KubernetesConfigMaps; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesConfigMaps(namespace).get(params).$promise, this.KubernetesConfigMaps(namespace).getYaml(params).$promise]); + const configMap = KubernetesConfigMapConverter.apiToConfigMap(raw, yaml); + return configMap; + } catch (err) { + if (err.status === 404) { + return KubernetesConfigMapConverter.defaultConfigMap(namespace, name); + } + throw new PortainerError('Unable to retrieve config map', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesConfigMaps(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesConfigMapConverter.apiToConfigMap(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve config maps', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(config) { + try { + const payload = KubernetesConfigMapConverter.createPayload(config); + const params = {}; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesConfigMaps(namespace).create(params, payload).$promise; + return KubernetesConfigMapConverter.apiToConfigMap(data); + } catch (err) { + throw new PortainerError('Unable to create config map', err); + } + } + + create(config) { + return this.$async(this.createAsync, config); + } + + /** + * UPDATE + */ + async updateAsync(config) { + try { + if (!config.Id) { + return await this.create(config); + } + const payload = KubernetesConfigMapConverter.updatePayload(config); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesConfigMaps(namespace).update(params, payload).$promise; + return KubernetesConfigMapConverter.apiToConfigMap(data); + } catch (err) { + throw new PortainerError('Unable to update config map', err); + } + } + update(config) { + return this.$async(this.updateAsync, config); + } + + /** + * DELETE + */ + async deleteAsync(config) { + try { + const params = new KubernetesCommonParams(); + params.id = config.Name; + const namespace = config.Namespace; + await this.KubernetesConfigMaps(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete config map', err); + } + } + + delete(config) { + return this.$async(this.deleteAsync, config); + } +} + +export default KubernetesConfigMapService; +angular.module('portainer.kubernetes').service('KubernetesConfigMapService', KubernetesConfigMapService); diff --git a/app/kubernetes/services/configurationService.js b/app/kubernetes/services/configurationService.js new file mode 100644 index 000000000..1f421bfbd --- /dev/null +++ b/app/kubernetes/services/configurationService.js @@ -0,0 +1,128 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration'; +import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap'; +import KubernetesSecretConverter from 'Kubernetes/converters/secret'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigurationService { + /* @ngInject */ + constructor($async, Authentication, KubernetesNamespaceService, KubernetesConfigMapService, KubernetesSecretService) { + this.$async = $async; + this.Authentication = Authentication; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesConfigMapService = KubernetesConfigMapService; + this.KubernetesSecretService = KubernetesSecretService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]); + let configuration; + if (secret.status === 'fulfilled') { + configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value); + return configuration; + } + configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value); + return configuration; + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]); + const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret)); + const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap)); + return _.concat(configMapsConfigurations, secretsConfigurations); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(formValues) { + try { + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + await this.KubernetesConfigMapService.create(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + await this.KubernetesSecretService.create(secret); + } + } catch (err) { + throw err; + } + } + + create(formValues) { + return this.$async(this.createAsync, formValues); + } + + /** + * UPDATE + */ + async updateAsync(formValues) { + try { + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + await this.KubernetesConfigMapService.update(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + await this.KubernetesSecretService.update(secret); + } + } catch (err) { + throw err; + } + } + update(config) { + return this.$async(this.updateAsync, config); + } + + /** + * DELETE + */ + async deleteAsync(config) { + try { + if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { + await this.KubernetesConfigMapService.delete(config); + } else { + await this.KubernetesSecretService.delete(config); + } + } catch (err) { + throw err; + } + } + + delete(config) { + return this.$async(this.deleteAsync, config); + } +} + +export default KubernetesConfigurationService; +angular.module('portainer.kubernetes').service('KubernetesConfigurationService', KubernetesConfigurationService); diff --git a/app/kubernetes/services/controllerRevisionService.js b/app/kubernetes/services/controllerRevisionService.js new file mode 100644 index 000000000..0f5d38fc9 --- /dev/null +++ b/app/kubernetes/services/controllerRevisionService.js @@ -0,0 +1,31 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesControllerRevisionService { + /* @ngInject */ + constructor($async, KubernetesControllerRevisions) { + this.$async = $async; + this.KubernetesControllerRevisions = KubernetesControllerRevisions; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesControllerRevisions(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve ControllerRevisions', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesControllerRevisionService; +angular.module('portainer.kubernetes').service('KubernetesControllerRevisionService', KubernetesControllerRevisionService); diff --git a/app/kubernetes/services/daemonSetService.js b/app/kubernetes/services/daemonSetService.js new file mode 100644 index 000000000..6fbc38e9c --- /dev/null +++ b/app/kubernetes/services/daemonSetService.js @@ -0,0 +1,133 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet'; + +class KubernetesDaemonSetService { + /* @ngInject */ + constructor($async, KubernetesDaemonSets) { + this.$async = $async; + this.KubernetesDaemonSets = KubernetesDaemonSets; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesDaemonSets(namespace).get(params).$promise, this.KubernetesDaemonSets(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve DaemonSet', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesDaemonSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve DaemonSets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(daemonSet) { + try { + const params = {}; + const payload = KubernetesDaemonSetConverter.createPayload(daemonSet); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesDaemonSets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create daemonset', err); + } + } + + create(daemonSet) { + return this.$async(this.createAsync, daemonSet); + } + + /** + * PATCH + */ + async patchAsync(oldDaemonSet, newDaemonSet) { + try { + const params = new KubernetesCommonParams(); + params.id = newDaemonSet.Name; + const namespace = newDaemonSet.Namespace; + const payload = KubernetesDaemonSetConverter.patchPayload(oldDaemonSet, newDaemonSet); + if (!payload.length) { + return; + } + const data = await this.KubernetesDaemonSets(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch daemonSet', err); + } + } + + patch(oldDaemonSet, newDaemonSet) { + return this.$async(this.patchAsync, oldDaemonSet, newDaemonSet); + } + + /** + * DELETE + */ + async deleteAsync(daemonSet) { + try { + const params = new KubernetesCommonParams(); + params.id = daemonSet.Name; + const namespace = daemonSet.Namespace; + await this.KubernetesDaemonSets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove daemonset', err); + } + } + + delete(daemonSet) { + return this.$async(this.deleteAsync, daemonSet); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesDaemonSets(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback daemonset', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesDaemonSetService; +angular.module('portainer.kubernetes').service('KubernetesDaemonSetService', KubernetesDaemonSetService); diff --git a/app/kubernetes/services/deploymentService.js b/app/kubernetes/services/deploymentService.js new file mode 100644 index 000000000..fe0f4ac3b --- /dev/null +++ b/app/kubernetes/services/deploymentService.js @@ -0,0 +1,133 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment'; + +class KubernetesDeploymentService { + /* @ngInject */ + constructor($async, KubernetesDeployments) { + this.$async = $async; + this.KubernetesDeployments = KubernetesDeployments; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesDeployments(namespace).get(params).$promise, this.KubernetesDeployments(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve Deployment', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesDeployments(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve Deployments', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(deployment) { + try { + const params = {}; + const payload = KubernetesDeploymentConverter.createPayload(deployment); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesDeployments(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create deployment', err); + } + } + + create(deployment) { + return this.$async(this.createAsync, deployment); + } + + /** + * PATCH + */ + async patchAsync(oldDeployment, newDeployment) { + try { + const params = new KubernetesCommonParams(); + params.id = newDeployment.Name; + const namespace = newDeployment.Namespace; + const payload = KubernetesDeploymentConverter.patchPayload(oldDeployment, newDeployment); + if (!payload.length) { + return; + } + const data = await this.KubernetesDeployments(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch deployment', err); + } + } + + patch(oldDeployment, newDeployment) { + return this.$async(this.patchAsync, oldDeployment, newDeployment); + } + + /** + * DELETE + */ + async deleteAsync(deployment) { + try { + const params = new KubernetesCommonParams(); + params.id = deployment.Name; + const namespace = deployment.Namespace; + await this.KubernetesDeployments(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove deployment', err); + } + } + + delete(deployment) { + return this.$async(this.deleteAsync, deployment); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesDeployments(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback deployment', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesDeploymentService; +angular.module('portainer.kubernetes').service('KubernetesDeploymentService', KubernetesDeploymentService); diff --git a/app/kubernetes/services/eventService.js b/app/kubernetes/services/eventService.js new file mode 100644 index 000000000..8d38eba0d --- /dev/null +++ b/app/kubernetes/services/eventService.js @@ -0,0 +1,34 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import KubernetesEventConverter from 'Kubernetes/converters/event'; + +class KubernetesEventService { + /* @ngInject */ + constructor($async, KubernetesEvents) { + this.$async = $async; + this.KubernetesEvents = KubernetesEvents; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesEvents(namespace).get().$promise; + const res = _.map(data.items, (item) => KubernetesEventConverter.apiToEvent(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve events', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesEventService; +angular.module('portainer.kubernetes').service('KubernetesEventService', KubernetesEventService); diff --git a/app/kubernetes/services/healthService.js b/app/kubernetes/services/healthService.js new file mode 100644 index 000000000..977ffae56 --- /dev/null +++ b/app/kubernetes/services/healthService.js @@ -0,0 +1,30 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesHealthService { + /* @ngInject */ + constructor($async, KubernetesHealth) { + this.$async = $async; + this.KubernetesHealth = KubernetesHealth; + + this.pingAsync = this.pingAsync.bind(this); + } + + /** + * PING + */ + async pingAsync() { + try { + return await this.KubernetesHealth.ping().$promise; + } catch (err) { + throw new PortainerError('Unable to retrieve environment health', err); + } + } + + ping() { + return this.$async(this.pingAsync); + } +} + +export default KubernetesHealthService; +angular.module('portainer.kubernetes').service('KubernetesHealthService', KubernetesHealthService); diff --git a/app/kubernetes/services/historyService.js b/app/kubernetes/services/historyService.js new file mode 100644 index 000000000..df0284166 --- /dev/null +++ b/app/kubernetes/services/historyService.js @@ -0,0 +1,54 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import KubernetesHistoryHelper from 'Kubernetes/helpers/history'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesHistoryService { + /* @ngInject */ + constructor($async, KubernetesReplicaSetService, KubernetesControllerRevisionService) { + this.$async = $async; + this.KubernetesReplicaSetService = KubernetesReplicaSetService; + this.KubernetesControllerRevisionService = KubernetesControllerRevisionService; + + this.getAsync = this.getAsync.bind(this); + } + + /** + * GET + */ + async getAsync(application) { + try { + const namespace = application.ResourcePool; + let rawRevisions; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + rawRevisions = await this.KubernetesReplicaSetService.get(namespace); + break; + case KubernetesApplicationTypes.DAEMONSET: + rawRevisions = await this.KubernetesControllerRevisionService.get(namespace); + break; + case KubernetesApplicationTypes.STATEFULSET: + rawRevisions = await this.KubernetesControllerRevisionService.get(namespace); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + + const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application); + application.CurrentRevision = currentRevision; + application.Revisions = revisionsList; + return application; + } catch (err) { + throw new PortainerError('', err); + } + } + + get(application) { + return this.$async(this.getAsync, application); + } +} + +export default KubernetesHistoryService; +angular.module('portainer.kubernetes').service('KubernetesHistoryService', KubernetesHistoryService); diff --git a/app/kubernetes/services/namespaceService.js b/app/kubernetes/services/namespaceService.js new file mode 100644 index 000000000..891cbd0b5 --- /dev/null +++ b/app/kubernetes/services/namespaceService.js @@ -0,0 +1,96 @@ +import _ from 'lodash-es'; + +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace'; +import $allSettled from 'Portainer/services/allSettled'; + +class KubernetesNamespaceService { + /* @ngInject */ + constructor($async, KubernetesNamespaces) { + this.$async = $async; + this.KubernetesNamespaces = KubernetesNamespaces; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesNamespaces().status(params).$promise; + const [raw, yaml] = await Promise.all([this.KubernetesNamespaces().get(params).$promise, this.KubernetesNamespaces().getYaml(params).$promise]); + return KubernetesNamespaceConverter.apiToNamespace(raw, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve namespace', err); + } + } + + async getAllAsync() { + try { + const data = await this.KubernetesNamespaces().get().$promise; + const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise); + const namespaces = await $allSettled(promises); + const visibleNamespaces = _.map(namespaces.fulfilled, (item) => { + if (item.status.phase !== 'Terminating') { + return KubernetesNamespaceConverter.apiToNamespace(item); + } + }); + return _.without(visibleNamespaces, undefined); + } catch (err) { + throw new PortainerError('Unable to retrieve namespaces', err); + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } + + /** + * CREATE + */ + async createAsync(namespace) { + try { + const payload = KubernetesNamespaceConverter.createPayload(namespace); + const params = {}; + const data = await this.KubernetesNamespaces().create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create namespace', err); + } + } + + create(namespace) { + return this.$async(this.createAsync, namespace); + } + + /** + * DELETE + */ + async deleteAsync(namespace) { + try { + const params = new KubernetesCommonParams(); + params.id = namespace.Name; + await this.KubernetesNamespaces().delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete namespace', err); + } + } + + delete(namespace) { + return this.$async(this.deleteAsync, namespace); + } +} + +export default KubernetesNamespaceService; +angular.module('portainer.kubernetes').service('KubernetesNamespaceService', KubernetesNamespaceService); diff --git a/app/kubernetes/services/nodeService.js b/app/kubernetes/services/nodeService.js new file mode 100644 index 000000000..58b2d787e --- /dev/null +++ b/app/kubernetes/services/nodeService.js @@ -0,0 +1,50 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +import PortainerError from 'Portainer/error'; +import KubernetesNodeConverter from 'Kubernetes/converters/node'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesNodeService { + /* @ngInject */ + constructor($async, KubernetesNodes) { + this.$async = $async; + this.KubernetesNodes = KubernetesNodes; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [details, yaml] = await Promise.all([this.KubernetesNodes().get(params).$promise, this.KubernetesNodes().getYaml(params).$promise]); + return KubernetesNodeConverter.apiToNodeDetails(details, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve node details', err); + } + } + + async getAllAsync() { + try { + const data = await this.KubernetesNodes().get().$promise; + return _.map(data.items, (item) => KubernetesNodeConverter.apiToNode(item)); + } catch (err) { + throw { msg: 'Unable to retrieve nodes', err: err }; + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } +} + +export default KubernetesNodeService; +angular.module('portainer.kubernetes').service('KubernetesNodeService', KubernetesNodeService); diff --git a/app/kubernetes/services/persistentVolumeClaimService.js b/app/kubernetes/services/persistentVolumeClaimService.js new file mode 100644 index 000000000..a19fcdf0d --- /dev/null +++ b/app/kubernetes/services/persistentVolumeClaimService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesPersistentVolumeClaimService { + /* @ngInject */ + constructor($async, EndpointProvider, KubernetesPersistentVolumeClaims) { + this.$async = $async; + this.EndpointProvider = EndpointProvider; + this.KubernetesPersistentVolumeClaims = KubernetesPersistentVolumeClaims; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([ + this.KubernetesPersistentVolumeClaims(namespace).get(params).$promise, + this.KubernetesPersistentVolumeClaims(namespace).getYaml(params).$promise, + ]); + const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(raw, storageClasses, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve persistent volume claim', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesPersistentVolumeClaims(namespace).get().$promise; + const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return _.map(data.items, (item) => KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(item, storageClasses)); + } catch (err) { + throw new PortainerError('Unable to retrieve persistent volume claims', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(claim) { + try { + const params = {}; + const payload = KubernetesPersistentVolumeClaimConverter.createPayload(claim); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesPersistentVolumeClaims(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create persistent volume claim', err); + } + } + + create(claim) { + return this.$async(this.createAsync, claim); + } + + /** + * PATCH + */ + async patchAsync(oldPVC, newPVC) { + try { + const params = new KubernetesCommonParams(); + params.id = newPVC.Name; + const namespace = newPVC.Namespace; + const payload = KubernetesPersistentVolumeClaimConverter.patchPayload(oldPVC, newPVC); + if (!payload.length) { + return; + } + const data = await this.KubernetesPersistentVolumeClaims(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch persistent volume claim', err); + } + } + + patch(oldPVC, newPVC) { + return this.$async(this.patchAsync, oldPVC, newPVC); + } + + /** + * DELETE + */ + async deleteAsync(pvc) { + try { + const params = new KubernetesCommonParams(); + params.id = pvc.Name; + const namespace = pvc.Namespace; + await this.KubernetesPersistentVolumeClaims(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete persistent volume claim', err); + } + } + + delete(pvc) { + return this.$async(this.deleteAsync, pvc); + } +} + +export default KubernetesPersistentVolumeClaimService; +angular.module('portainer.kubernetes').service('KubernetesPersistentVolumeClaimService', KubernetesPersistentVolumeClaimService); diff --git a/app/kubernetes/services/podService.js b/app/kubernetes/services/podService.js new file mode 100644 index 000000000..49c733e62 --- /dev/null +++ b/app/kubernetes/services/podService.js @@ -0,0 +1,75 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesPodConverter from 'Kubernetes/converters/pod'; + +class KubernetesPodService { + /* @ngInject */ + constructor($async, KubernetesPods) { + this.$async = $async; + this.KubernetesPods = KubernetesPods; + + this.getAllAsync = this.getAllAsync.bind(this); + this.logsAsync = this.logsAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + /** + * GET ALL + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesPods(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesPodConverter.apiToPod(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve pods', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } + + /** + * Logs + * + * @param {string} namespace + * @param {string} podName + */ + async logsAsync(namespace, podName) { + try { + const params = new KubernetesCommonParams(); + params.id = podName; + const data = await this.KubernetesPods(namespace).logs(params).$promise; + return data.logs.length === 0 ? [] : data.logs.split('\n'); + } catch (err) { + throw new PortainerError('Unable to retrieve pod logs', err); + } + } + + logs(namespace, podName) { + return this.$async(this.logsAsync, namespace, podName); + } + + /** + * DELETE + */ + async deleteAsync(pod) { + try { + const params = new KubernetesCommonParams(); + params.id = pod.Name; + const namespace = pod.Namespace; + await this.KubernetesPods(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove pod', err); + } + } + + delete(pod) { + return this.$async(this.deleteAsync, pod); + } +} + +export default KubernetesPodService; +angular.module('portainer.kubernetes').service('KubernetesPodService', KubernetesPodService); diff --git a/app/kubernetes/services/replicaSetService.js b/app/kubernetes/services/replicaSetService.js new file mode 100644 index 000000000..a6f65bc06 --- /dev/null +++ b/app/kubernetes/services/replicaSetService.js @@ -0,0 +1,31 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesReplicaSetService { + /* @ngInject */ + constructor($async, KubernetesReplicaSets) { + this.$async = $async; + this.KubernetesReplicaSets = KubernetesReplicaSets; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesReplicaSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve ReplicaSets', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesReplicaSetService; +angular.module('portainer.kubernetes').service('KubernetesReplicaSetService', KubernetesReplicaSetService); diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js new file mode 100644 index 000000000..7dbfb933b --- /dev/null +++ b/app/kubernetes/services/resourcePoolService.js @@ -0,0 +1,113 @@ +import _ from 'lodash-es'; +import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; + +import angular from 'angular'; +import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'; +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; +import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; + +class KubernetesResourcePoolService { + /* @ngInject */ + constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService) { + this.$async = $async; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const namespace = await this.KubernetesNamespaceService.get(name); + const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } + return pool; + } catch (err) { + throw err; + } + } + + async getAllAsync() { + try { + const namespaces = await this.KubernetesNamespaceService.get(); + const pools = await Promise.all( + _.map(namespaces, async (namespace) => { + const name = namespace.Name; + const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } + return pool; + }) + ); + return pools; + } catch (err) { + throw err; + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } + + /** + * CREATE + */ + // TODO: review LimitRange future + async createAsync(name, owner, hasQuota, cpuLimit, memoryLimit) { + try { + const namespace = new KubernetesNamespace(); + namespace.Name = name; + namespace.ResourcePoolName = name; + namespace.ResourcePoolOwner = owner; + await this.KubernetesNamespaceService.create(namespace); + if (hasQuota) { + const quota = new KubernetesResourceQuota(name); + quota.CpuLimit = cpuLimit; + quota.MemoryLimit = memoryLimit; + quota.ResourcePoolName = name; + quota.ResourcePoolOwner = owner; + await this.KubernetesResourceQuotaService.create(quota); + } + } catch (err) { + throw err; + } + } + + create(name, owner, hasQuota, cpuLimit, memoryLimit) { + return this.$async(this.createAsync, name, owner, hasQuota, cpuLimit, memoryLimit); + } + + /** + * DELETE + */ + async deleteAsync(pool) { + try { + await this.KubernetesNamespaceService.delete(pool.Namespace); + } catch (err) { + throw err; + } + } + + delete(pool) { + return this.$async(this.deleteAsync, pool); + } +} + +export default KubernetesResourcePoolService; +angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService); diff --git a/app/kubernetes/services/resourceQuotaService.js b/app/kubernetes/services/resourceQuotaService.js new file mode 100644 index 000000000..0a41da03f --- /dev/null +++ b/app/kubernetes/services/resourceQuotaService.js @@ -0,0 +1,109 @@ +import _ from 'lodash-es'; + +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota'; + +class KubernetesResourceQuotaService { + /* @ngInject */ + constructor($async, KubernetesResourceQuotas) { + this.$async = $async; + this.KubernetesResourceQuotas = KubernetesResourceQuotas; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesResourceQuotas(namespace).get(params).$promise, this.KubernetesResourceQuotas(namespace).getYaml(params).$promise]); + return KubernetesResourceQuotaConverter.apiToResourceQuota(raw, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve resource quota', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesResourceQuotas(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesResourceQuotaConverter.apiToResourceQuota(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve resource quotas', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(quota) { + try { + const payload = KubernetesResourceQuotaConverter.createPayload(quota); + const namespace = payload.metadata.namespace; + const params = {}; + const data = await this.KubernetesResourceQuotas(namespace).create(params, payload).$promise; + return KubernetesResourceQuotaConverter.apiToResourceQuota(data); + } catch (err) { + throw new PortainerError('Unable to create quota', err); + } + } + + create(quota) { + return this.$async(this.createAsync, quota); + } + + /** + * UPDATE + */ + async updateAsync(quota) { + try { + const payload = KubernetesResourceQuotaConverter.updatePayload(quota); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesResourceQuotas(namespace).update(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to update resource quota', err); + } + } + + update(quota) { + return this.$async(this.updateAsync, quota); + } + + /** + * DELETE + */ + async deleteAsync(quota) { + try { + const params = new KubernetesCommonParams(); + params.id = quota.Name; + await this.KubernetesResourceQuotas(quota.Namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete quota', err); + } + } + + delete(quota) { + return this.$async(this.deleteAsync, quota); + } +} + +export default KubernetesResourceQuotaService; +angular.module('portainer.kubernetes').service('KubernetesResourceQuotaService', KubernetesResourceQuotaService); diff --git a/app/kubernetes/services/secretService.js b/app/kubernetes/services/secretService.js new file mode 100644 index 000000000..b54ca5aad --- /dev/null +++ b/app/kubernetes/services/secretService.js @@ -0,0 +1,110 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesSecretConverter from 'Kubernetes/converters/secret'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesSecretService { + /* @ngInject */ + constructor($async, KubernetesSecrets) { + this.$async = $async; + this.KubernetesSecrets = KubernetesSecrets; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesSecrets(namespace).get(params).$promise, this.KubernetesSecrets(namespace).getYaml(params).$promise]); + const secret = KubernetesSecretConverter.apiToSecret(raw, yaml); + return secret; + } catch (err) { + throw new PortainerError('Unable to retrieve secret', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesSecrets(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesSecretConverter.apiToSecret(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve secrets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(secret) { + try { + const payload = KubernetesSecretConverter.createPayload(secret); + const namespace = payload.metadata.namespace; + const params = {}; + const data = await this.KubernetesSecrets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create secret', err); + } + } + + create(secret) { + return this.$async(this.createAsync, secret); + } + + /** + * UPDATE + */ + async updateAsync(secret) { + try { + const payload = KubernetesSecretConverter.updatePayload(secret); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesSecrets(namespace).update(params, payload).$promise; + return KubernetesSecretConverter.apiToSecret(data); + } catch (err) { + throw new PortainerError('Unable to update secret', err); + } + } + + update(secret) { + return this.$async(this.updateAsync, secret); + } + + /** + * DELETE + */ + async deleteAsync(secret) { + try { + const params = new KubernetesCommonParams(); + params.id = secret.Name; + const namespace = secret.Namespace; + await this.KubernetesSecrets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete secret', err); + } + } + + delete(secret) { + return this.$async(this.deleteAsync, secret); + } +} + +export default KubernetesSecretService; +angular.module('portainer.kubernetes').service('KubernetesSecretService', KubernetesSecretService); diff --git a/app/kubernetes/services/serviceService.js b/app/kubernetes/services/serviceService.js new file mode 100644 index 000000000..a85292d76 --- /dev/null +++ b/app/kubernetes/services/serviceService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesServiceConverter from 'Kubernetes/converters/service'; + +class KubernetesServiceService { + /* @ngInject */ + constructor($async, KubernetesServices) { + this.$async = $async; + this.KubernetesServices = KubernetesServices; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesServices(namespace).get(params).$promise, this.KubernetesServices(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve service', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesServices(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve services', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(service) { + try { + const params = {}; + const payload = KubernetesServiceConverter.createPayload(service); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesServices(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create service', err); + } + } + + create(service) { + return this.$async(this.createAsync, service); + } + + /** + * PATCH + */ + async patchAsync(oldService, newService) { + try { + const params = new KubernetesCommonParams(); + params.id = newService.Name; + const namespace = newService.Namespace; + const payload = KubernetesServiceConverter.patchPayload(oldService, newService); + if (!payload.length) { + return; + } + const data = await this.KubernetesServices(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch service', err); + } + } + + patch(oldService, newService) { + return this.$async(this.patchAsync, oldService, newService); + } + + /** + * DELETE + */ + async deleteAsync(service) { + try { + const params = new KubernetesCommonParams(); + params.id = service.Name; + const namespace = service.Namespace; + await this.KubernetesServices(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove service', err); + } + } + + delete(service) { + return this.$async(this.deleteAsync, service); + } +} + +export default KubernetesServiceService; +angular.module('portainer.kubernetes').service('KubernetesServiceService', KubernetesServiceService); diff --git a/app/kubernetes/services/stackService.js b/app/kubernetes/services/stackService.js new file mode 100644 index 000000000..23e09c01f --- /dev/null +++ b/app/kubernetes/services/stackService.js @@ -0,0 +1,32 @@ +import _ from 'lodash-es'; +import angular from 'angular'; + +class KubernetesStackService { + /* @ngInject */ + constructor($async, KubernetesApplicationService) { + this.$async = $async; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const applications = await this.KubernetesApplicationService.get(namespace); + const stacks = _.map(applications, (item) => item.StackName); + return _.uniq(_.without(stacks, '-')); + } catch (err) { + throw err; + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesStackService; +angular.module('portainer.kubernetes').service('KubernetesStackService', KubernetesStackService); diff --git a/app/kubernetes/services/statefulSetService.js b/app/kubernetes/services/statefulSetService.js new file mode 100644 index 000000000..60cdea44c --- /dev/null +++ b/app/kubernetes/services/statefulSetService.js @@ -0,0 +1,144 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; + +class KubernetesStatefulSetService { + /* @ngInject */ + constructor($async, KubernetesStatefulSets, KubernetesServiceService) { + this.$async = $async; + this.KubernetesStatefulSets = KubernetesStatefulSets; + this.KubernetesServiceService = KubernetesServiceService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesStatefulSets(namespace).get(params).$promise, this.KubernetesStatefulSets(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + const headlessServiceName = raw.spec.serviceName; + if (headlessServiceName) { + try { + const headlessService = await this.KubernetesServiceService.get(namespace, headlessServiceName); + res.Yaml += '---\n' + headlessService.Yaml; + } catch (error) { + // if has error means headless service does not exist + // skip error as we don't care in this case + } + } + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve StatefulSet', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesStatefulSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve StatefulSets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(statefulSet) { + try { + const params = {}; + const payload = KubernetesStatefulSetConverter.createPayload(statefulSet); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesStatefulSets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create statefulSet', err); + } + } + + create(statefulSet) { + return this.$async(this.createAsync, statefulSet); + } + + /** + * PATCH + */ + async patchAsync(oldStatefulSet, newStatefulSet) { + try { + const params = new KubernetesCommonParams(); + params.id = newStatefulSet.Name; + const namespace = newStatefulSet.Namespace; + const payload = KubernetesStatefulSetConverter.patchPayload(oldStatefulSet, newStatefulSet); + if (!payload.length) { + return; + } + const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch statefulSet', err); + } + } + + patch(oldStatefulSet, newStatefulSet) { + return this.$async(this.patchAsync, oldStatefulSet, newStatefulSet); + } + + /** + * DELETE + */ + async deleteAsync(statefulSet) { + try { + const params = new KubernetesCommonParams(); + params.id = statefulSet.Name; + const namespace = statefulSet.Namespace; + await this.KubernetesStatefulSets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove statefulSet', err); + } + } + + delete(statefulSet) { + return this.$async(this.deleteAsync, statefulSet); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesStatefulSets(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback statefulSet', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesStatefulSetService; +angular.module('portainer.kubernetes').service('KubernetesStatefulSetService', KubernetesStatefulSetService); diff --git a/app/kubernetes/services/storageService.js b/app/kubernetes/services/storageService.js new file mode 100644 index 000000000..ea2a5f053 --- /dev/null +++ b/app/kubernetes/services/storageService.js @@ -0,0 +1,37 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesStorageClassConverter from 'Kubernetes/converters/storageClass'; + +class KubernetesStorageService { + /* @ngInject */ + constructor($async, KubernetesStorage) { + this.$async = $async; + this.KubernetesStorage = KubernetesStorage; + + this.getAsync = this.getAsync.bind(this); + } + + /** + * GET + */ + async getAsync(endpointId) { + try { + const params = { + endpointId: endpointId, + }; + const classes = await this.KubernetesStorage().get(params).$promise; + const res = _.map(classes.items, (item) => KubernetesStorageClassConverter.apiToStorageClass(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve storage classes', err); + } + } + + get(endpointId) { + return this.$async(this.getAsync, endpointId); + } +} + +export default KubernetesStorageService; +angular.module('portainer.kubernetes').service('KubernetesStorageService', KubernetesStorageService); diff --git a/app/kubernetes/services/volumeService.js b/app/kubernetes/services/volumeService.js new file mode 100644 index 000000000..b65b4cb93 --- /dev/null +++ b/app/kubernetes/services/volumeService.js @@ -0,0 +1,70 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +import KubernetesVolumeConverter from 'Kubernetes/converters/volume'; + +class KubernetesVolumeService { + /* @ngInject */ + constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService) { + this.$async = $async; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [pvc, pool] = await Promise.all([await this.KubernetesPersistentVolumeClaimService.get(namespace, name), await this.KubernetesResourcePoolService.get(namespace)]); + return KubernetesVolumeConverter.pvcToVolume(pvc, pool); + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const pools = await this.KubernetesResourcePoolService.get(namespace); + const res = await Promise.all( + _.map(pools, async (pool) => { + const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); + return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool)); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * DELETE + */ + async deleteAsync(volume) { + try { + await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim); + } catch (err) { + throw err; + } + } + + delete(volume) { + return this.$async(this.deleteAsync, volume); + } +} + +export default KubernetesVolumeService; +angular.module('portainer.kubernetes').service('KubernetesVolumeService', KubernetesVolumeService); diff --git a/app/kubernetes/views/applications/applications.html b/app/kubernetes/views/applications/applications.html new file mode 100644 index 000000000..d60033ade --- /dev/null +++ b/app/kubernetes/views/applications/applications.html @@ -0,0 +1,60 @@ + + Applications + + + + +
+ + +

+ + As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster. +

+

+ +

+
+
+ +
+
+ + + + + Applications + + + + + Port mappings + + + + + Stacks + + + + + + +
+
+
diff --git a/app/kubernetes/views/applications/applications.js b/app/kubernetes/views/applications/applications.js new file mode 100644 index 000000000..e4b0de22c --- /dev/null +++ b/app/kubernetes/views/applications/applications.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsView', { + templateUrl: './applications.html', + controller: 'KubernetesApplicationsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js new file mode 100644 index 000000000..859698a00 --- /dev/null +++ b/app/kubernetes/views/applications/applicationsController.js @@ -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); diff --git a/app/kubernetes/views/applications/console/console.html b/app/kubernetes/views/applications/console/console.html new file mode 100644 index 000000000..51bb6ea3e --- /dev/null +++ b/app/kubernetes/views/applications/console/console.html @@ -0,0 +1,71 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Pods > + {{ ctrl.podName }} > Console + + + + +
+
+
+ + +
+
+ Console +
+ +
+ +
+ + + + +
+
+ +
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/applications/console/console.js b/app/kubernetes/views/applications/console/console.js new file mode 100644 index 000000000..2a44c97b4 --- /dev/null +++ b/app/kubernetes/views/applications/console/console.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', { + templateUrl: './console.html', + controller: 'KubernetesApplicationConsoleController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/console/consoleController.js b/app/kubernetes/views/applications/console/consoleController.js new file mode 100644 index 000000000..ccf8e37ed --- /dev/null +++ b/app/kubernetes/views/applications/console/consoleController.js @@ -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); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html new file mode 100644 index 000000000..d9421b904 --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -0,0 +1,904 @@ + + Applications > Create an application + + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Edit + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

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').

+
+

An application with the same name already exists inside the selected resource pool.

+
+
+ + + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ Resource pool +
+ + +
+ +
+ +
+
+
+
+ + 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. +
+
+ + +
+ Stack +
+ +
+
+ + 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. +
+
+ + +
+ +
+ +
+
+ + +
+ Environment +
+ + +
+
+ + + add environment variable + +
+ +
+
+
+
+ name + +
+
+ +

Environment variable name is required.

+
+

This environment variable is already defined.

+
+
+ +
+ value + +
+ +
+ + +
+
+
+
+ + +
+ Configurations +
+ + +
+
+ + + add configuration + +
+
+ + 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. +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+
+
+ The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: + + {{ key }}{{ $last ? '' : ', ' }} + +
+
+ + + +
+
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +

Path is required.

+
+

This path is already used.

+
+
+ +
+ + +
+
+
+ +
+ + + +
+ Persisting data +
+ +
+
+ + No storage option is available to persist data, contact your administrator to enable a storage option. +
+
+ + +
+
+ + + add persisted folder + +
+ +
+
+
+ path in container + +
+ +
+ requested size + + + + +
+ +
+ storage + + +
+ +
+ + +
+
+ +
+
+
+ +

Path is required.

+
+

This path is already defined.

+
+
+ +
+
+ +

Size is required.

+

This value must be greater than zero.

+
+
+
+ +
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ Specify how the data will be used across instances. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ Resource reservations +
+ +
+
+ + Resource reservations are applied per instance of the application. +
+
+ +
+
+ + 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. +
+
+ +
+
+ + 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. +
+
+ + +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.state.sliders.memory.min }} and {{ ctrl.state.sliders.memory.max }} +

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ + +
+ Deployment +
+ +
+
+ Select how you want to deploy your application inside the cluster. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+ + +
+
+ + This application will reserve the following resources: {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. +
+
+ +
+
+ + This application would exceed available resources. Please review resource reservations or the instance count. +
+
+ +
+
+ + The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. +
+
+ +
+ Publishing the application +
+ +
+
+ Select how you want to publish your application. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + + publish a new port + +
+ +
+ + 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 30000-32767. +
+ +
+
+
+ container port + +
+ +
+ node port + +
+ +
+ load balancer port + +
+ +
+
+ + +
+ +
+
+ +
+
+
+
+

Container port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+
+
+ +
+
+
+

Node port number must be inside the range 30000-32767.

+

Node port number must be inside the range 30000-32767.

+
+
+
+ +
+
+
+

Load balancer port number is required.

+

Load balancer port number must be inside the range 1-65535.

+

Load balancer port number must be inside the range 1-65535.

+
+
+
+ +
+
+
+
+ + +
+ Actions +
+ +
+
+ + +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/applications/create/createApplication.js b/app/kubernetes/views/applications/create/createApplication.js new file mode 100644 index 000000000..382b0227a --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplication.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateApplicationView', { + templateUrl: './createApplication.html', + controller: 'KubernetesCreateApplicationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js new file mode 100644 index 000000000..15ad90b2b --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -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); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html new file mode 100644 index 000000000..0ea9118ba --- /dev/null +++ b/app/kubernetes/views/applications/edit/application.html @@ -0,0 +1,477 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > {{ ctrl.application.Name }} + + + + +
+
+
+ + + + + Application +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ ctrl.application.Name }} + external +
Stack{{ ctrl.application.StackName }}
Resource pool + {{ ctrl.application.ResourcePool }} + system +
Deployment + Replicated + Global + {{ ctrl.application.RunningPodsCount }} / {{ ctrl.application.TotalPodsCount }} +
+
Resource reservations
+
per instance
+
+
CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}
+
Memory {{ ctrl.application.Requests.Memory | humansize }}
+
Creation + {{ ctrl.application.ApplicationOwner }} + {{ ctrl.application.CreationDate | getisodate }} +
+
+
+
+ Note + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ +
+ + + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+ + + +
+ + +
+ Accessing the application +
+ +
+ + This application is not exposing any port. +
+ +
+
+
+ + This application is exposed through an external load balancer. Use the links below to access the different ports exposed. +
+
+ +

Load balancer status: pending

+

+ what does the "pending" status means? + + +

+
+ +

Load balancer status: available

+

+ Load balancer IP address: {{ ctrl.application.LoadBalancerIPAddress }} + + Copy + + +

+
+
+
+ + + + + + + + + + + +
Container portLoad balancer port
{{ port.targetPort }} + {{ port.port }} + + access + +
+
+
+ +
+
+ + 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. +
+
+ + + + + + + + + + + +
Container portCluster node port
{{ port.targetPort }}{{ port.nodePort }}
+
+
+ +
+
+ + This application is only available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }} + Copy + +
+
+

Refer to the below port configuration to access the application.

+
+
+ + + + + + + + + + + + + +
Container portApplication portProtocol
{{ port.targetPort }}{{ port.port }}{{ port.protocol }}
+
+
+
+ + +
Auto-scaling
+ +
+ + This application does not have an autoscaling policy defined. +
+ +
+
+ + + + + + + + + + + + + +
Minimum instancesMaximum instances + Target CPU usage + +
{{ ctrl.application.AutoScaler.MinReplicas }}{{ ctrl.application.AutoScaler.MaxReplicas }}{{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}%
+
+
+ + + +
Configuration
+ +
+ + This application is not using any environment variable or configuration. +
+ +
+
+ + + + + + + + + + + + + +
Environment variableValueConfiguration
{{ envvar.name }} + {{ envvar.value }} + {{ envvar.valueFrom.configMapKeyRef.key }} + {{ envvar.valueFrom.secretKeyRef.key }} + {{ envvar.valueFrom.fieldRef.fieldPath }} (downward API) + - + + - + {{ envvar.valueFrom.configMapKeyRef.name }} + {{ envvar.valueFrom.secretKeyRef.name }} +
+
+
+ +
+ + + + + + + + + + + + + + +
Configuration pathValueConfiguration
+ {{ volume.fileMountPath }} + {{ volume.configurationKey ? volume.configurationKey : '-' }} + {{ volume.configurationName }} +
+
+ + + +
+ Data persistence +
+ +
+ + This application has no persisted folders. +
+ +
+
+ Data access policy: + {{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyText }} + +
+ + + + + + + + + + + + + +
Persisted folderPersistence
+ {{ volume.MountPath }} + + {{ volume.PersistentVolumeClaimName }} + {{ volume.HostPath }} on host filesystem
+ + + + + + + + + + + + + + + + + +
PodPersisted folderPersistence
+ {{ pod.Name }} + + {{ volume.MountPath }} + + + {{ volume.PersistentVolumeClaimName + '-' + pod.Name }} + {{ volume.HostPath }} on host filesystem
+ +
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/applications/edit/application.js b/app/kubernetes/views/applications/edit/application.js new file mode 100644 index 000000000..ae935f0f4 --- /dev/null +++ b/app/kubernetes/views/applications/edit/application.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationView', { + templateUrl: './application.html', + controller: 'KubernetesApplicationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js new file mode 100644 index 000000000..0aa896c25 --- /dev/null +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -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); diff --git a/app/kubernetes/views/applications/logs/logs.html b/app/kubernetes/views/applications/logs/logs.html new file mode 100644 index 000000000..eaefc6417 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logs.html @@ -0,0 +1,62 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Pods > + {{ ctrl.podName }} > Logs + + + + +
+
+
+ + +
+
+ Actions +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+

{{ line }}

No log line matching the '{{ ctrl.state.search }}' filter

No logs available

+
+
+
diff --git a/app/kubernetes/views/applications/logs/logs.js b/app/kubernetes/views/applications/logs/logs.js new file mode 100644 index 000000000..513bf9285 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logs.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationLogsView', { + templateUrl: './logs.html', + controller: 'KubernetesApplicationLogsController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/logs/logsController.js b/app/kubernetes/views/applications/logs/logsController.js new file mode 100644 index 000000000..a7d761dd5 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logsController.js @@ -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); diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html new file mode 100644 index 000000000..eeb43f9da --- /dev/null +++ b/app/kubernetes/views/cluster/cluster.html @@ -0,0 +1,40 @@ + + Cluster information + + + + +
+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ +
+
+
diff --git a/app/kubernetes/views/cluster/cluster.js b/app/kubernetes/views/cluster/cluster.js new file mode 100644 index 000000000..688cf877e --- /dev/null +++ b/app/kubernetes/views/cluster/cluster.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesClusterView', { + templateUrl: './cluster.html', + controller: 'KubernetesClusterController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js new file mode 100644 index 000000000..2a8313a87 --- /dev/null +++ b/app/kubernetes/views/cluster/clusterController.js @@ -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); diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html new file mode 100644 index 000000000..6fabaaa77 --- /dev/null +++ b/app/kubernetes/views/cluster/node/node.html @@ -0,0 +1,107 @@ + + Cluster > {{ ctrl.node.Name }} + + + + +
+
+
+ + + + + Node + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Hostname{{ ctrl.node.Name }}
Role{{ ctrl.node.Role }}
Kubelet version{{ ctrl.node.Version }}
Creation date{{ ctrl.node.CreationDate | getisodate }}
Status + + {{ ctrl.node.Status }} + + + {{ ctrl.node.Conditions | kubernetesNodeConditionsMessage }} + +
+
+ + +
+
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/cluster/node/node.js b/app/kubernetes/views/cluster/node/node.js new file mode 100644 index 000000000..38fb14ede --- /dev/null +++ b/app/kubernetes/views/cluster/node/node.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesNodeView', { + templateUrl: './node.html', + controller: 'KubernetesNodeController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/cluster/node/nodeController.js b/app/kubernetes/views/cluster/node/nodeController.js new file mode 100644 index 000000000..eb4bb7327 --- /dev/null +++ b/app/kubernetes/views/cluster/node/nodeController.js @@ -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); diff --git a/app/kubernetes/views/configurations/configurations.html b/app/kubernetes/views/configurations/configurations.html new file mode 100644 index 000000000..8e55b4fc8 --- /dev/null +++ b/app/kubernetes/views/configurations/configurations.html @@ -0,0 +1,19 @@ + + Configurations + + + + +
+
+
+ +
+
+
diff --git a/app/kubernetes/views/configurations/configurations.js b/app/kubernetes/views/configurations/configurations.js new file mode 100644 index 000000000..98538965b --- /dev/null +++ b/app/kubernetes/views/configurations/configurations.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationsView', { + templateUrl: './configurations.html', + controller: 'KubernetesConfigurationsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/configurations/configurationsController.js b/app/kubernetes/views/configurations/configurationsController.js new file mode 100644 index 000000000..eb94722fe --- /dev/null +++ b/app/kubernetes/views/configurations/configurationsController.js @@ -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); diff --git a/app/kubernetes/views/configurations/create/createConfiguration.html b/app/kubernetes/views/configurations/create/createConfiguration.html new file mode 100644 index 000000000..446f7a334 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfiguration.html @@ -0,0 +1,135 @@ + + Configurations > Create a configuration + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end + with an alphanumeric character.

+
+

A configuration with the same name already exists inside the selected resource pool.

+
+
+ + +
+ Resource pool +
+ + +
+ +
+ +
+
+
+
+ + 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. +
+
+ + +
+ Configuration type +
+ +
+
+ Select the type of data that you want to save in the configuration. +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + + + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/configurations/create/createConfiguration.js b/app/kubernetes/views/configurations/create/createConfiguration.js new file mode 100644 index 000000000..515c39a81 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfiguration.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateConfigurationView', { + templateUrl: './createConfiguration.html', + controller: 'KubernetesCreateConfigurationController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/configurations/create/createConfigurationController.js b/app/kubernetes/views/configurations/create/createConfigurationController.js new file mode 100644 index 000000000..696a2efc7 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfigurationController.js @@ -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); diff --git a/app/kubernetes/views/configurations/edit/configuration.html b/app/kubernetes/views/configurations/edit/configuration.html new file mode 100644 index 000000000..3d97546ea --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configuration.html @@ -0,0 +1,120 @@ + + Resource pools > + {{ ctrl.configuration.Namespace }} > + Configurations > {{ ctrl.configuration.Name }} + + + + +
+
+
+ + + + + Configuration +
+ + + + + + + + + + + + + + + +
Name + {{ ctrl.configuration.Name }} +
Resource Pool + {{ ctrl.configuration.Namespace }} + system +
Configuration type + {{ ctrl.configuration.Type | kubernetesConfigurationTypeText }} +
+
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+ + + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/configurations/edit/configuration.js b/app/kubernetes/views/configurations/edit/configuration.js new file mode 100644 index 000000000..3fbee48b8 --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configuration.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationView', { + templateUrl: './configuration.html', + controller: 'KubernetesConfigurationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js new file mode 100644 index 000000000..3906d9f59 --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configurationController.js @@ -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); diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html new file mode 100644 index 000000000..697c19e6c --- /dev/null +++ b/app/kubernetes/views/configure/configure.html @@ -0,0 +1,122 @@ + + Endpoints > {{ ctrl.endpoint.Name }} > Kubernetes configuration + + + + +
+
+
+ + +
+
+ Expose applications over external IP addresses +
+
+ + Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider. +

+ + Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs. +

+
+
+
+
+ + +
+
+ +
+ Available storage options +
+ +
+
+ + Unable to detect any storage class available to persist data. Users won't be able to persist application data inside this cluster. +
+
+ +
+ +

+ 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. +

+

+ You can find more information about access modes + in the official Kubernetes documentation. +

+
+
+ +
+
+ + + + + + + + + + + +
StorageShared access policy
+
+ + {{ class.Name }} +
+
+ + +
+
+
+ + + Shared access policy configuration required + +
+
+ +
+ Actions +
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js new file mode 100644 index 000000000..53f876166 --- /dev/null +++ b/app/kubernetes/views/configure/configureController.js @@ -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); diff --git a/app/kubernetes/views/dashboard/dashboard.html b/app/kubernetes/views/dashboard/dashboard.html new file mode 100644 index 000000000..776479e3d --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboard.html @@ -0,0 +1,99 @@ + + Endpoint summary + + + + + diff --git a/app/kubernetes/views/dashboard/dashboard.js b/app/kubernetes/views/dashboard/dashboard.js new file mode 100644 index 000000000..76b30ec2e --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboard.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesDashboardView', { + templateUrl: './dashboard.html', + controller: 'KubernetesDashboardController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/dashboard/dashboardController.js b/app/kubernetes/views/dashboard/dashboardController.js new file mode 100644 index 000000000..8cc6e2cb9 --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboardController.js @@ -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); diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html new file mode 100644 index 000000000..2f657ab54 --- /dev/null +++ b/app/kubernetes/views/deploy/deploy.html @@ -0,0 +1,118 @@ + + Deploy Kubernetes resources + + + + +
+ + +
+
+ + + + + Deploy + +
+
+ +
+ +
+
+ +
+ Deployment type +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ Web editor +
+
+ +

+ + Portainer uses Kompose 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. +

+

+ You can get more information about Compose file format in the + official documentation. +

+
+ + You can get more information about Kubernetes file format in the + official documentation. + +
+
+
+ +
+
+ + +
+ Actions +
+
+
+ +
+
+ +
+
+ + + Logs +
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/deploy/deploy.js b/app/kubernetes/views/deploy/deploy.js new file mode 100644 index 000000000..f02365586 --- /dev/null +++ b/app/kubernetes/views/deploy/deploy.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesDeployView', { + templateUrl: './deploy.html', + controller: 'KubernetesDeployController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js new file mode 100644 index 000000000..a37200df9 --- /dev/null +++ b/app/kubernetes/views/deploy/deployController.js @@ -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); diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html new file mode 100644 index 000000000..3044762b0 --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html @@ -0,0 +1,116 @@ + + Resource pools > + {{ ctrl.pool.Namespace.Name }} > Access management + + + + +
+
+
+ + + + + + + + + + +
Name + {{ ctrl.pool.Namespace.Name }} +
+
+
+
+
+ +
+
+ + + +
+
+ +
+ + No user nor team access has been set on the endpoint. Head over to the + endpoint access view to manage them. + + + +
+
+ + + + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + + +
+
+
diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js new file mode 100644 index 000000000..cda4acfbd --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolAccessView', { + templateUrl: './resourcePoolAccess.html', + controller: 'KubernetesResourcePoolAccessController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js b/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js new file mode 100644 index 000000000..e6f443913 --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js @@ -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); diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html new file mode 100644 index 000000000..052b7c888 --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -0,0 +1,163 @@ + + Resource pools > Create a resource pool + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end + with an alphanumeric character.

+
+

A resource pool with the same name already exists.

+
+
+ +
+ Quota +
+ +
+
+

+ + 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. +

+
+
+ + +
+
+
+ +

At least a single limit must be set for the quota to be valid.

+
+
+ +
+
+ Resource limits +
+
+ +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+ +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.js b/app/kubernetes/views/resource-pools/create/createResourcePool.js new file mode 100644 index 000000000..daf67bd9c --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', { + templateUrl: './createResourcePool.html', + controller: 'KubernetesCreateResourcePoolController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js new file mode 100644 index 000000000..e614cc878 --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -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); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html new file mode 100644 index 000000000..c27b9d162 --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -0,0 +1,189 @@ + + Resource pools > {{ ctrl.pool.Namespace.Name }} + + + + +
+
+
+ + + + + Resource pool +
+ +
+ +
+ +
+
+ +
Quota
+ +
+
+ + +
+
+
+ +

At least a single limit must be set for the quota to be valid.

+
+
+ +
+
+ Resource limits +
+
+ +
+ +
+ +
+
+ +
+
+

+ Memory limit (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.defaults.MemoryLimit }} and + {{ ctrl.state.sliderMaxMemory }}

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+
+ + +
+ +
+ Actions +
+
+
+ +
+
+ +
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.js b/app/kubernetes/views/resource-pools/edit/resourcePool.js new file mode 100644 index 000000000..3b6012a2d --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', { + templateUrl: './resourcePool.html', + controller: 'KubernetesResourcePoolController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js new file mode 100644 index 000000000..790200d5c --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -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); diff --git a/app/kubernetes/views/resource-pools/resourcePools.html b/app/kubernetes/views/resource-pools/resourcePools.html new file mode 100644 index 000000000..1c6dce94d --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePools.html @@ -0,0 +1,19 @@ + + Resource pools + + + + +
+
+
+ +
+
+
diff --git a/app/kubernetes/views/resource-pools/resourcePools.js b/app/kubernetes/views/resource-pools/resourcePools.js new file mode 100644 index 000000000..030e1e6eb --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePools.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView', { + templateUrl: './resourcePools.html', + controller: 'KubernetesResourcePoolsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/resource-pools/resourcePoolsController.js b/app/kubernetes/views/resource-pools/resourcePoolsController.js new file mode 100644 index 000000000..0f3de973a --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePoolsController.js @@ -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); diff --git a/app/kubernetes/views/stacks/logs/logs.html b/app/kubernetes/views/stacks/logs/logs.html new file mode 100644 index 000000000..cab652fb9 --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logs.html @@ -0,0 +1,60 @@ + + Resource pools + > {{ ctrl.state.transition.namespace }} > + Applications > Stacks > {{ ctrl.state.transition.name }} > Logs + + + + +
+
+
+ + +
+
+ Actions +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+

{{ line.AppName }} {{ line.Line }}

No log line matching the '{{ ctrl.state.search }}' filter

No logs available

+
+
+
diff --git a/app/kubernetes/views/stacks/logs/logs.js b/app/kubernetes/views/stacks/logs/logs.js new file mode 100644 index 000000000..bd8a1dcf1 --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logs.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesStackLogsView', { + templateUrl: './logs.html', + controller: 'KubernetesStackLogsController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/stacks/logs/logsController.js b/app/kubernetes/views/stacks/logs/logsController.js new file mode 100644 index 000000000..bcb7a2c1e --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logsController.js @@ -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); diff --git a/app/kubernetes/views/volumes/edit/volume.html b/app/kubernetes/views/volumes/edit/volume.html new file mode 100644 index 000000000..781457f1f --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volume.html @@ -0,0 +1,99 @@ + + Resource pools > + {{ ctrl.volume.ResourcePool.Namespace.Name }} > + Volumes > {{ ctrl.volume.PersistentVolumeClaim.Name }} + + + + +
+
+
+ + + + + Volume + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ ctrl.volume.PersistentVolumeClaim.Name }} + external + unused +
Resource pool + {{ ctrl.volume.ResourcePool.Namespace.Name }} + system +
Storage{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}
Size{{ ctrl.volume.PersistentVolumeClaim.Storage }}
Creation date{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}
+
+
+ + + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/volumes/edit/volume.js b/app/kubernetes/views/volumes/edit/volume.js new file mode 100644 index 000000000..a3d850a65 --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volume.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumeView', { + templateUrl: './volume.html', + controller: 'KubernetesVolumeController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/volumes/edit/volumeController.js b/app/kubernetes/views/volumes/edit/volumeController.js new file mode 100644 index 000000000..a91be622c --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volumeController.js @@ -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); diff --git a/app/kubernetes/views/volumes/volumes.html b/app/kubernetes/views/volumes/volumes.html new file mode 100644 index 000000000..4c9ba2f81 --- /dev/null +++ b/app/kubernetes/views/volumes/volumes.html @@ -0,0 +1,20 @@ + + Volumes + + + + +
+
+
+ + +
+
+
diff --git a/app/kubernetes/views/volumes/volumes.js b/app/kubernetes/views/volumes/volumes.js new file mode 100644 index 000000000..98829ddc1 --- /dev/null +++ b/app/kubernetes/views/volumes/volumes.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumesView', { + templateUrl: './volumes.html', + controller: 'KubernetesVolumesController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/volumes/volumesController.js b/app/kubernetes/views/volumes/volumesController.js new file mode 100644 index 000000000..2fcb899a3 --- /dev/null +++ b/app/kubernetes/views/volumes/volumesController.js @@ -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); diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 2a43cb0dd..face617cb 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -8,7 +8,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat // to have more controls on which URL should trigger the unauthenticated state. $rootScope.$on('unauthenticated', function (event, data) { if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) { - $state.go('portainer.auth', { error: 'Your session has expired' }); + $state.go('portainer.logout', { error: 'Your session has expired' }); } }); @@ -106,8 +106,7 @@ angular.module('portainer.app', []).config([ name: 'portainer.auth', url: '/auth', params: { - logout: false, - error: '', + reload: false, }, views: { 'content@': { @@ -118,6 +117,22 @@ angular.module('portainer.app', []).config([ 'sidebar@': {}, }, }; + const logout = { + name: 'portainer.logout', + url: '/logout', + params: { + error: '', + performApiLogout: false, + }, + views: { + 'content@': { + templateUrl: './views/logout/logout.html', + controller: 'LogoutController', + controllerAs: 'ctrl', + }, + 'sidebar@': {}, + }, + }; var endpoints = { name: 'portainer.endpoints', @@ -141,6 +156,18 @@ angular.module('portainer.app', []).config([ }, }; + const endpointKubernetesConfiguration = { + name: 'portainer.endpoints.endpoint.kubernetesConfig', + url: '/configure', + views: { + 'content@': { + templateUrl: '../kubernetes/views/configure/configure.html', + controller: 'KubernetesConfigureController', + controllerAs: 'ctrl', + }, + }, + }; + var endpointCreation = { name: 'portainer.endpoints.new', url: '/new', @@ -235,6 +262,7 @@ angular.module('portainer.app', []).config([ 'content@': { templateUrl: './views/init/endpoint/initEndpoint.html', controller: 'InitEndpointController', + controllerAs: 'ctrl', }, }, }; @@ -491,10 +519,12 @@ angular.module('portainer.app', []).config([ $stateRegistryProvider.register(about); $stateRegistryProvider.register(account); $stateRegistryProvider.register(authentication); + $stateRegistryProvider.register(logout); $stateRegistryProvider.register(endpoints); $stateRegistryProvider.register(endpoint); $stateRegistryProvider.register(endpointAccess); $stateRegistryProvider.register(endpointCreation); + $stateRegistryProvider.register(endpointKubernetesConfiguration); $stateRegistryProvider.register(groups); $stateRegistryProvider.register(group); $stateRegistryProvider.register(groupAccess); diff --git a/app/portainer/components/access-datatable/accessDatatable.html b/app/portainer/components/access-datatable/accessDatatable.html index 55169a342..3f92dbecc 100644 --- a/app/portainer/components/access-datatable/accessDatatable.html +++ b/app/portainer/components/access-datatable/accessDatatable.html @@ -69,12 +69,14 @@ > - + {{ item.Name }} - inherited - override + inherited + override {{ item.Type }} diff --git a/app/portainer/components/access-datatable/accessDatatableController.js b/app/portainer/components/access-datatable/accessDatatableController.js index ec5846ad5..653c5a436 100644 --- a/app/portainer/components/access-datatable/accessDatatableController.js +++ b/app/portainer/components/access-datatable/accessDatatableController.js @@ -6,7 +6,7 @@ angular.module('portainer.app').controller('AccessDatatableController', [ angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); this.disableRemove = function (item) { - return item.Inherited; + return item.Inherited && this.inheritFrom; }; this.allowSelection = function (item) { diff --git a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js index 97497a7fc..8667482e5 100644 --- a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js +++ b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership'; -import { ResourceControlTypeString as RCTS, ResourceControlTypeInt as RCTI } from 'Portainer/models/resourceControl/resourceControlTypes'; +import { ResourceControlTypeInt as RCTI, ResourceControlTypeString as RCTS } from 'Portainer/models/resourceControl/resourceControlTypes'; import { AccessControlPanelData } from './porAccessControlPanelModel'; angular.module('portainer.app').controller('porAccessControlPanelController', [ diff --git a/app/portainer/components/accessManagement/porAccessManagement.html b/app/portainer/components/accessManagement/porAccessManagement.html index 1ac0fdb99..5d46339bc 100644 --- a/app/portainer/components/accessManagement/porAccessManagement.html +++ b/app/portainer/components/accessManagement/porAccessManagement.html @@ -27,7 +27,6 @@
-
-
diff --git a/app/portainer/components/accessManagement/porAccessManagementController.js b/app/portainer/components/accessManagement/porAccessManagementController.js index a76a62991..1234d3f3c 100644 --- a/app/portainer/components/accessManagement/porAccessManagementController.js +++ b/app/portainer/components/accessManagement/porAccessManagementController.js @@ -53,27 +53,14 @@ class PorAccessManagementController { } async $onInit() { - const entity = this.accessControlledEntity; - if (!entity) { - this.Notifications.error('Failure', 'Unable to retrieve accesses'); - return; - } - if (!entity.UserAccessPolicies) { - entity.UserAccessPolicies = {}; - } - if (!entity.TeamAccessPolicies) { - entity.TeamAccessPolicies = {}; - } - const parent = this.inheritFrom; - if (parent && !parent.UserAccessPolicies) { - parent.UserAccessPolicies = {}; - } - if (parent && !parent.TeamAccessPolicies) { - parent.TeamAccessPolicies = {}; - } - this.roles = []; - this.rbacEnabled = false; try { + const entity = this.accessControlledEntity; + const parent = this.inheritFrom; + // TODO: refactor + // extract this code and locate it in AccessService.accesses() function + // see resourcePoolAccessController for another usage of AccessService.accesses() + // which needs RBAC support + this.roles = []; this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC); if (this.rbacEnabled) { this.roles = await this.RoleService.roles(); @@ -81,13 +68,7 @@ class PorAccessManagementController { selectedRole: this.roles[0], }; } - const data = await this.AccessService.accesses( - entity.UserAccessPolicies, - entity.TeamAccessPolicies, - parent ? parent.UserAccessPolicies : {}, - parent ? parent.TeamAccessPolicies : {}, - this.roles - ); + const data = await this.AccessService.accesses(entity, parent, this.roles); this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc'); this.authorizedUsersAndTeams = data.authorizedUsersAndTeams; } catch (err) { diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index b12bc5bd4..89367a114 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -6,6 +6,7 @@ function isBetween(value, a, b) { return (value >= a && value <= b) || (value >= b && value <= a); } +// TODO: review - refactor to use a class that can be extended angular.module('portainer.app').controller('GenericDatatableController', [ '$interval', 'PaginationService', diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index fa67430e5..6b4a5aeb3 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -1,8 +1,14 @@
- + + @@ -22,6 +28,9 @@ {{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }} + + {{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }} + @@ -69,12 +78,36 @@
-
+
No snapshot available
+
+ + + {{ $ctrl.model.Kubernetes.Snapshots[0].TotalCPU }} CPU + + {{ $ctrl.model.Kubernetes.Snapshots[0].TotalMemory | humansize }} RAM + + + + + Kubernetes {{ $ctrl.model.Kubernetes.Snapshots[0].KubernetesVersion }} + + + {{ $ctrl.model.Kubernetes.Snapshots[0].NodeCount }} {{ $ctrl.model.Kubernetes.Snapshots[0].NodeCount === 1 ? 'node' : 'nodes' }} + + +
+ +
+ + - + +
+
diff --git a/app/portainer/components/header-content.js b/app/portainer/components/header-content.js index 188e28209..448222503 100644 --- a/app/portainer/components/header-content.js +++ b/app/portainer/components/header-content.js @@ -8,7 +8,7 @@ angular.module('portainer.app').directive('rdHeaderContent', [ scope.username = Authentication.getUserDetails().username; }, template: - '', + '', restrict: 'E', }; return directive; diff --git a/app/portainer/components/slider/sliderController.js b/app/portainer/components/slider/sliderController.js index 4541a2762..4ac47d9f6 100644 --- a/app/portainer/components/slider/sliderController.js +++ b/app/portainer/components/slider/sliderController.js @@ -1,21 +1,45 @@ -angular.module('portainer.app').controller('SliderController', function () { - var ctrl = this; +// TODO: k8s merge - TEST WITH EXISTING SLIDERS ! +// Not sure if this is not breaking existing sliders on docker views +// Or sliders with onChange call (docker service update view) +import angular from 'angular'; - ctrl.options = { - floor: ctrl.floor, - ceil: ctrl.ceil, - step: ctrl.step, - precision: ctrl.precision, - showSelectionBar: true, - enforceStep: false, - translate: function (value, sliderId, label) { - if ((label === 'floor' && ctrl.floor === 0) || value === 0) { - return 'unlimited'; - } - return value; - }, - onChange: function () { - ctrl.onChange(); - }, - }; -}); +class SliderController { + /* @ngInject */ + constructor($scope) { + this.$scope = $scope; + + this.buildOptions = this.buildOptions.bind(this); + this.translate = this.translate.bind(this); + } + + $onChanges() { + this.buildOptions(); + } + + translate(value, sliderId, label) { + if ((label === 'floor' && this.floor === 0) || value === 0) { + return 'unlimited'; + } + return value; + } + + buildOptions() { + this.options = { + floor: this.floor, + ceil: this.ceil, + step: this.step, + precision: this.precision, + showSelectionBar: true, + enforceStep: false, + translate: this.translate, + onChange: () => this.onChange(), + }; + } + + $onInit() { + this.buildOptions(); + } +} + +export default SliderController; +angular.module('portainer.app').controller('SliderController', SliderController); diff --git a/app/portainer/components/tooltip.js b/app/portainer/components/tooltip.js index 6609986da..3c9c9e4c4 100644 --- a/app/portainer/components/tooltip.js +++ b/app/portainer/components/tooltip.js @@ -4,9 +4,12 @@ angular.module('portainer.app').directive('portainerTooltip', [ scope: { message: '@', position: '@', + customStyle: '', + template: ` + + + `, restrict: 'E', }; return directive; diff --git a/app/portainer/error.js b/app/portainer/error.js new file mode 100644 index 000000000..88c57d72b --- /dev/null +++ b/app/portainer/error.js @@ -0,0 +1,6 @@ +export default class PortainerError { + constructor(msg, err) { + this.msg = msg; + this.err = err; + } +} diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js index 034dba32c..f31705979 100644 --- a/app/portainer/filters/filters.js +++ b/app/portainer/filters/filters.js @@ -126,11 +126,13 @@ angular return function (type) { if (type === 1) { return 'Docker'; - } else if (type === 2) { + } else if (type === 2 || type === 6) { return 'Agent'; } else if (type === 3) { return 'Azure ACI'; - } else if (type === 4) { + } else if (type === 5) { + return 'Kubernetes'; + } else if (type === 4 || type === 7) { return 'Edge Agent'; } return ''; @@ -143,6 +145,8 @@ angular return 'fab fa-microsoft'; } else if (type === 4) { return 'fa fa-cloud'; + } else if (type === 5 || type === 6 || type === 7) { + return 'fas fa-dharmachakra'; } return 'fab fa-docker'; }; diff --git a/app/portainer/models/endpoint/formValues.js b/app/portainer/models/endpoint/formValues.js new file mode 100644 index 000000000..f7268ffb9 --- /dev/null +++ b/app/portainer/models/endpoint/formValues.js @@ -0,0 +1,41 @@ +import { PortainerEndpointConnectionTypes } from 'Portainer/models/endpoint/models'; + +export class PortainerEndpointInitFormValues { + constructor() { + this.ConnectionType = PortainerEndpointConnectionTypes.KUBERNETES_LOCAL; + this.Name = ''; + this.URL = ''; + this.TLS = false; + this.TLSSkipVerify = false; + this.TLSSKipClientVerify = false; + this.TLSCACert = null; + this.TLSCert = null; + this.TLSKey = null; + this.AzureApplicationId = ''; + this.AzureTenantId = ''; + this.AzureAuthenticationKey = ''; + } +} + +class PortainerEndpointInitFormValueEndpointSection { + constructor(value, title, classes, description) { + this.Id = value; + this.Value = value; + this.Title = title; + this.Classes = classes; + this.Description = description; + } +} + +export const PortainerEndpointInitFormValueEndpointSections = Object.freeze([ + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.DOCKER_LOCAL, 'Docker', 'fab fa-docker', 'Manage the local Docker environment'), + new PortainerEndpointInitFormValueEndpointSection( + PortainerEndpointConnectionTypes.KUBERNETES_LOCAL, + 'Kubernetes', + 'fas fa-dharmachakra', + 'Manage the local Kubernetes environment' + ), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.REMOTE, 'Remote', 'fab fa-docker', 'Manage a remote Docker environment'), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AGENT, 'Agent', 'fa fa-bolt', 'Connect to a Portainer agent'), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AZURE, 'Azure', 'fab fa-microsoft', 'Connect to Microsoft Azure ACI'), +]); diff --git a/app/portainer/models/endpoint/models.js b/app/portainer/models/endpoint/models.js new file mode 100644 index 000000000..feae2b9ab --- /dev/null +++ b/app/portainer/models/endpoint/models.js @@ -0,0 +1,28 @@ +/** + * JS reference of portainer.go#EndpointType iota + */ +export const PortainerEndpointTypes = Object.freeze({ + // DockerEnvironment represents an endpoint connected to a Docker environment + DockerEnvironment: 1, + // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment + AgentOnDockerEnvironment: 2, + // AzureEnvironment represents an endpoint connected to an Azure environment + AzureEnvironment: 3, + // EdgeAgentOnDockerEnvironment represents an endpoint connected to an Edge agent deployed on a Docker environment + EdgeAgentOnDockerEnvironment: 4, + // KubernetesLocalEnvironment represents an endpoint connected to a local Kubernetes environment + KubernetesLocalEnvironment: 5, + // AgentOnKubernetesEnvironment represents an endpoint connected to a Portainer agent deployed on a Kubernetes environment + AgentOnKubernetesEnvironment: 6, + // EdgeAgentOnKubernetesEnvironment represents an endpoint connected to an Edge agent deployed on a Kubernetes environment + EdgeAgentOnKubernetesEnvironment: 7, +}); + +export const PortainerEndpointConnectionTypes = Object.freeze({ + DOCKER_LOCAL: 1, + KUBERNETES_LOCAL: 2, + REMOTE: 3, + AZURE: 4, + AGENT: 5, + EDGE: 6, +}); diff --git a/app/portainer/rest/auth.js b/app/portainer/rest/auth.js index 5169084c2..1723ccd1f 100644 --- a/app/portainer/rest/auth.js +++ b/app/portainer/rest/auth.js @@ -4,13 +4,11 @@ angular.module('portainer.app').factory('Auth', [ function AuthFactory($resource, API_ENDPOINT_AUTH) { 'use strict'; return $resource( - API_ENDPOINT_AUTH, + API_ENDPOINT_AUTH + '/:action', {}, { - login: { - method: 'POST', - ignoreLoadingBar: true, - }, + login: { method: 'POST', ignoreLoadingBar: true }, + logout: { method: 'POST', params: { action: 'logout' }, ignoreLoadingBar: true }, } ); }, diff --git a/app/portainer/services/allSettled.js b/app/portainer/services/allSettled.js new file mode 100644 index 000000000..020869dfa --- /dev/null +++ b/app/portainer/services/allSettled.js @@ -0,0 +1,34 @@ +import _ from 'lodash-es'; + +/** + * + * @param {any[]} promises + */ +export default async function $allSettled(promises) { + const res = { + fulfilled: [], + rejected: [], + }; + const data = await Promise.allSettled(promises); + res.fulfilled = _.reduce( + data, + (acc, item) => { + if (item.status === 'fulfilled') { + acc.push(item.value); + } + return acc; + }, + [] + ); + res.rejected = _.reduce( + data, + (acc, item) => { + if (item.status === 'rejected') { + acc.push(item.reason); + } + return acc; + }, + [] + ); + return res; +} diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js index 457d6bbfc..2c9acd0dd 100644 --- a/app/portainer/services/api/accessService.js +++ b/app/portainer/services/api/accessService.js @@ -4,9 +4,10 @@ import { TeamAccessViewModel } from '../../models/access'; angular.module('portainer.app').factory('AccessService', [ '$q', + '$async', 'UserService', 'TeamService', - function AccessServiceFactory($q, UserService, TeamService) { + function AccessServiceFactory($q, $async, UserService, TeamService) { 'use strict'; var service = {}; @@ -15,6 +16,7 @@ angular.module('portainer.app').factory('AccessService', [ const role = _.find(roles, (role) => role.Id === roleId); return role ? role : { Id: 0, Name: '-' }; } + return { Id: 0, Name: '-' }; } function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies, roles) { @@ -50,7 +52,7 @@ angular.module('portainer.app').factory('AccessService', [ }; } - service.accesses = function (authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) { + function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) { var deferred = $q.defer(); $q.all({ @@ -80,7 +82,36 @@ angular.module('portainer.app').factory('AccessService', [ }); return deferred.promise; - }; + } + + async function accessesAsync(entity, parent, roles) { + try { + if (!entity) { + throw { msg: 'Unable to retrieve accesses' }; + } + if (!entity.UserAccessPolicies) { + entity.UserAccessPolicies = {}; + } + if (!entity.TeamAccessPolicies) { + entity.TeamAccessPolicies = {}; + } + if (parent && !parent.UserAccessPolicies) { + parent.UserAccessPolicies = {}; + } + if (parent && !parent.TeamAccessPolicies) { + parent.TeamAccessPolicies = {}; + } + return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}, roles); + } catch (err) { + throw err; + } + } + + function accesses(entity, parent, roles) { + return $async(accessesAsync, entity, parent, roles); + } + + service.accesses = accesses; service.generateAccessPolicies = function (userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) { const newUserPolicies = _.clone(userAccessPolicies); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 1501e9ed8..3bbdf567f 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -1,3 +1,5 @@ +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; + angular.module('portainer.app').factory('EndpointService', [ '$q', 'Endpoints', @@ -57,7 +59,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 1, '', '', 1, [], false) + FileUploadService.createEndpoint('local', PortainerEndpointTypes.DockerEnvironment, '', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) @@ -86,7 +88,11 @@ angular.module('portainer.app').factory('EndpointService', [ var deferred = $q.defer(); var endpointURL = URL; - if (type !== 4) { + if ( + type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && + type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment && + type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { endpointURL = 'tcp://' + URL; } @@ -115,6 +121,20 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; + service.createLocalKubernetesEndpoint = function () { + var deferred = $q.defer(); + + FileUploadService.createEndpoint('local', 5, '', '', 1, [], true, true, true) + .then(function success(response) { + deferred.resolve(response.data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create endpoint', err: err }); + }); + + return deferred.promise; + }; + service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) { var deferred = $q.defer(); diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js index 375b6ee4f..b59280d50 100644 --- a/app/portainer/services/api/registryService.js +++ b/app/portainer/services/api/registryService.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; -import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry'; +import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry'; angular.module('portainer.app').factory('RegistryService', [ '$q', diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 31bf27548..47a9b6751 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -1,17 +1,17 @@ import _ from 'lodash-es'; -import { StackViewModel, ExternalStackViewModel } from '../../models/stack'; +import { ExternalStackViewModel, StackViewModel } from '../../models/stack'; angular.module('portainer.app').factory('StackService', [ '$q', + '$async', 'Stack', - 'ResourceControlService', 'FileUploadService', 'StackHelper', 'ServiceService', 'ContainerService', 'SwarmService', 'EndpointProvider', - function StackServiceFactory($q, Stack, ResourceControlService, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService, EndpointProvider) { + function StackServiceFactory($q, $async, Stack, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService, EndpointProvider) { 'use strict'; var service = {}; @@ -252,7 +252,6 @@ angular.module('portainer.app').factory('StackService', [ return deferred.promise; }; - service.createComposeStackFromFileContent = function (name, stackFileContent, env, endpointId) { var payload = { Name: name, @@ -333,6 +332,23 @@ angular.module('portainer.app').factory('StackService', [ return action(name, stackFileContent, env, endpointId); }; + async function kubernetesDeployAsync(endpointId, namespace, content, compose) { + try { + const payload = { + StackFileContent: content, + ComposeFormat: compose, + Namespace: namespace, + }; + await Stack.create({ method: 'undefined', type: 3, endpointId: endpointId }, payload).$promise; + } catch (err) { + throw { err: err }; + } + } + + service.kubernetesDeploy = function (endpointId, namespace, content, compose) { + return $async(kubernetesDeployAsync, endpointId, namespace, content, compose); + }; + return service; }, ]); diff --git a/app/portainer/services/api/tagService.js b/app/portainer/services/api/tagService.js index a62dfc3ff..ccc4551b0 100644 --- a/app/portainer/services/api/tagService.js +++ b/app/portainer/services/api/tagService.js @@ -2,8 +2,9 @@ import { TagViewModel } from '../../models/tag'; angular.module('portainer.app').factory('TagService', [ '$q', + '$async', 'Tags', - function TagServiceFactory($q, Tags) { + function TagServiceFactory($q, $async, Tags) { 'use strict'; var service = {}; @@ -37,7 +38,7 @@ angular.module('portainer.app').factory('TagService', [ return deferred.promise; }; - service.createTag = async function (name) { + async function createTagAsync(name) { var payload = { Name: name, }; @@ -47,7 +48,12 @@ angular.module('portainer.app').factory('TagService', [ } catch (err) { throw { msg: 'Unable to create tag', err }; } - }; + } + + function createTag(name) { + return $async(createTagAsync, name); + } + service.createTag = createTag; service.deleteTag = function (id) { return Tags.remove({ id: id }).$promise; diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index ecb6b610d..a5f1294fa 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -34,13 +34,21 @@ angular.module('portainer.app').factory('Authentication', [ } } - function logout() { + async function logoutAsync(performApiLogout) { + if (performApiLogout) { + await Auth.logout().$promise; + } + StateManager.clean(); EndpointProvider.clean(); LocalStorage.clean(); LocalStorage.storeLoginStateUUID(''); } + function logout(performApiLogout) { + return $async(logoutAsync, performApiLogout); + } + function init() { return $async(initAsync); } diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index c3b448f49..1cd99cb6d 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -126,6 +126,13 @@ angular.module('portainer.app').factory('LocalStorage', [ getJobImage: function () { return localStorageService.get('job_image'); }, + storeActiveTab: function (key, index) { + return localStorageService.set('active_tab_' + key, index); + }, + getActiveTab: function (key) { + const activeTab = localStorageService.get('active_tab_' + key); + return activeTab === null ? 0 : activeTab; + }, storeLogoutReason: (reason) => localStorageService.set('logout_reason', reason), getLogoutReason: () => localStorageService.get('logout_reason'), cleanLogoutReason: () => localStorageService.remove('logout_reason'), diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 2a9e40f64..663f564d7 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -128,6 +128,21 @@ angular.module('portainer.app').factory('ModalService', [ }); }; + service.confirmUpdate = function (message, callback) { + message = $sanitize(message); + service.confirm({ + title: 'Are you sure ?', + message: message, + buttons: { + confirm: { + label: 'Update', + className: 'btn-warning', + }, + }, + callback: callback, + }); + }; + service.confirmContainerDeletion = function (title, callback) { title = $sanitize(title); prompt({ diff --git a/app/portainer/services/notifications.js b/app/portainer/services/notifications.js index 3fb564b53..fdfd85e85 100644 --- a/app/portainer/services/notifications.js +++ b/app/portainer/services/notifications.js @@ -16,7 +16,9 @@ angular.module('portainer.app').factory('Notifications', [ service.error = function (title, e, fallbackText) { var msg = fallbackText; - if (e.err && e.err.data && e.err.data.details) { + if (e.err && e.err.data && e.err.data.message) { + msg = e.err.data.message; + } else if (e.err && e.err.data && e.err.data.details) { msg = e.err.data.details; } else if (e.data && e.data.details) { msg = e.data.details; @@ -26,8 +28,6 @@ angular.module('portainer.app').factory('Notifications', [ msg = e.data.content; } else if (e.message) { msg = e.message; - } else if (e.err && e.err.data && e.err.data.message) { - msg = e.err.data.message; } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { msg = e.err.data[0].message; } else if (e.err && e.err.data && e.err.data.err) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index e46502334..8b71ca6ee 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -173,6 +173,12 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); return deferred.promise; + } else if (endpoint.Type === 5 || endpoint.Type === 6 || endpoint.Type === 7) { + state.endpoint.name = endpoint.Name; + state.endpoint.mode = { provider: 'KUBERNETES' }; + LocalStorage.storeEndpointState(state.endpoint); + deferred.resolve(); + return deferred.promise; } $q.all({ diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index ce5d31d60..21b39af9d 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,3 +1,4 @@ +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -19,6 +20,7 @@ angular $scope.state = { EnvironmentType: 'agent', actionInProgress: false, + deploymentTab: 0, allowCreateTag: Authentication.isAdmin(), availableEdgeAgentCheckinOptions: [ { key: 'Use default interval', value: 0 }, @@ -54,9 +56,12 @@ angular }; $scope.copyAgentCommand = function () { - clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); - $('#copyNotification').show(); - $('#copyNotification').fadeOut(2000); + if ($scope.state.deploymentTab === 0) { + clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); + } else { + clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml'); + } + $('#copyNotification').show().fadeOut(2500); }; $scope.setDefaultPortainerInstanceURL = function () { @@ -67,6 +72,20 @@ angular $scope.formValues.URL = ''; }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + }; + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id); + } catch (err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + $scope.addDockerEndpoint = function () { var name = $scope.formValues.Name; var URL = $filter('stripprotocol')($scope.formValues.URL); @@ -83,7 +102,7 @@ angular var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, 1, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint(name, PortainerEndpointTypes.DockerEnvironment, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; $scope.addAgentEndpoint = function () { @@ -93,7 +112,9 @@ angular var groupId = $scope.formValues.GroupId; var tagIds = $scope.formValues.TagIds; - addEndpoint(name, 2, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); + addEndpoint(name, PortainerEndpointTypes.AgentOnDockerEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); + // TODO: k8s merge - temporarily updated to AgentOnKubernetesEnvironment, breaking Docker agent support + // addEndpoint(name, PortainerEndpointTypes.AgentOnKubernetesEnvironment, URL, publicURL, groupId, tags, true, true, true, null, null, null); }; $scope.addEdgeAgentEndpoint = function () { @@ -102,7 +123,9 @@ angular var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); + addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); + // TODO: k8s merge - temporarily updated to EdgeAgentOnKubernetesEnvironment, breaking Docker Edge agent support + // addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, URL, "", groupId, tags, false, false, false, null, null, null); }; $scope.addAzureEndpoint = function () { @@ -116,20 +139,6 @@ angular createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds); }; - $scope.onCreateTag = function onCreateTag(tagName) { - return $async(onCreateTagAsync, tagName); - }; - - async function onCreateTagAsync(tagName) { - try { - const tag = await TagService.createTag(tagName); - $scope.availableTags = $scope.availableTags.concat(tag); - $scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id); - } catch (err) { - Notifications.error('Failue', err, 'Unable to create tag'); - } - } - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { $scope.state.actionInProgress = true; EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) @@ -164,8 +173,10 @@ angular ) .then(function success(data) { Notifications.success('Endpoint created', name); - if (type === 4) { + if (type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { $state.go('portainer.endpoints.endpoint', { id: data.Id }); + } else if (type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) { + $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: data.Id }); } else { $state.go('portainer.endpoints', {}, { reload: true }); } diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index dd01e6142..06a1c62dd 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -75,13 +75,25 @@ Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it.
- - curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent - - Copy - - - + + + + curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml + + + + + + curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent + + + +
+ Copy command + + + +
@@ -92,8 +104,11 @@
- Allows you to create an endpoint that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the - required information on how to connect an Edge agent to this endpoint will be available after endpoint creation. +

+ Allows you to create an endpoint that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the + required information on how to connect an Edge agent to this endpoint will be available after endpoint creation. +

+

You can read more about the Edge agent in the userguide available here.

@@ -127,7 +142,15 @@
- +
@@ -146,7 +169,8 @@ + > +
+
-
@@ -319,13 +345,8 @@
- + +
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 367ace6f8..48fabddf8 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -10,11 +10,11 @@
- +

- This Edge endpoint is associated to an Edge environment. + This Edge endpoint is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}.

Edge key: {{ endpoint.EdgeKey }} @@ -24,11 +24,11 @@

- +

- Deploy the Edge agent on your remote Docker environment using the following command(s) + Deploy the Edge agent on your remote Docker/Kubernetes environment using the following command(s)

The agent will communicate with Portainer via {{ edgeKeyDetails.instanceURL }} and tcp://{{ edgeKeyDetails.tunnelServerAddr }} @@ -41,6 +41,11 @@ {{ dockerCommands.swarm }} + + + curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }} + +

Copy command @@ -66,6 +71,13 @@
+ + + + You should configure the features available in this Kubernetes environment in the + Kubernetes configuration view. + +
@@ -80,22 +92,23 @@
- +
-
+
-
+
-
+
+ -
Metadata
@@ -161,7 +175,7 @@
-
+
Security
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 561f38893..30f6c900d 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import uuidv4 from 'uuid/v4'; +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -24,6 +25,10 @@ angular uploadInProgress: false, actionInProgress: false, deploymentTab: 0, + azureEndpoint: false, + kubernetesEndpoint: false, + agentEndpoint: false, + edgeEndpoint: false, allowCreate: Authentication.isAdmin(), availableEdgeAgentCheckinOptions: [ { key: 'Use default interval', value: 0 }, @@ -58,7 +63,7 @@ angular $scope.endpoint.EdgeKey + ' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent' ); - } else { + } else if ($scope.state.deploymentTab === 1) { clipboard.copyText( 'docker network create --driver overlay portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' + $scope.randomEdgeID + @@ -66,6 +71,8 @@ angular $scope.endpoint.EdgeKey + " -e CAP_HOST_MANAGEMENT=1 --mode global --constraint 'node.platform.os == linux' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=//,dst=/host --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent" ); + } else { + clipboard.copyText('curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | bash -s -- ' + $scope.randomEdgeID + ' ' + $scope.endpoint.EdgeKey); } $('#copyNotificationDeploymentCommand').show().fadeOut(2500); }; @@ -114,7 +121,12 @@ angular AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey, }; - if ($scope.endpointType !== 'local' && endpoint.Type !== 3) { + if ( + $scope.endpointType !== 'local' && + endpoint.Type !== PortainerEndpointTypes.AzureEnvironment && + endpoint.Type !== PortainerEndpointTypes.KubernetesLocalEnvironment && + endpoint.Type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment + ) { payload.URL = 'tcp://' + endpoint.URL; } @@ -151,6 +163,30 @@ angular return keyInformation; } + function configureState() { + if ( + $scope.endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $scope.state.kubernetesEndpoint = true; + } + if ($scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { + $scope.state.edgeEndpoint = true; + } + if ($scope.endpoint.Type === PortainerEndpointTypes.AzureEnvironment) { + $scope.state.azureEndpoint = true; + } + if ( + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnDockerEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $scope.state.agentEndpoint = true; + } + } + function initView() { $q.all({ endpoint: EndpointService.endpoint($transition$.params().id), @@ -166,7 +202,7 @@ angular $scope.endpointType = 'remote'; } endpoint.URL = $filter('stripprotocol')(endpoint.URL); - if (endpoint.Type === 4) { + if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { $scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey); $scope.randomEdgeID = uuidv4(); $scope.dockerCommands = { @@ -180,6 +216,7 @@ angular $scope.endpoint = endpoint; $scope.groups = data.groups; $scope.availableTags = data.tags; + configureState(); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 489254d9b..1b14629ec 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -10,6 +10,8 @@ + +

diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index af41523f7..24d2efcf6 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -15,7 +15,8 @@ angular LegacyExtensionManager, ModalService, MotdService, - SystemService + SystemService, + KubernetesHealthService ) { $scope.state = { connectingToEdgeEndpoint: false, @@ -30,6 +31,10 @@ angular return switchToAzureEndpoint(endpoint); } else if (endpoint.Type === 4) { return switchToEdgeEndpoint(endpoint); + } else if (endpoint.Type === 5 || endpoint.Type === 6) { + return switchToKubernetesEndpoint(endpoint); + } else if (endpoint.Type === 7) { + return switchToKubernetesEdgeEndpoint(endpoint); } checkEndpointStatus(endpoint) @@ -102,6 +107,17 @@ angular }); } + function switchToKubernetesEndpoint(endpoint) { + EndpointProvider.setEndpointID(endpoint.Id); + StateManager.updateEndpointState(endpoint, []) + .then(function success() { + $state.go('kubernetes.dashboard'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Kubernetes endpoint'); + }); + } + function switchToEdgeEndpoint(endpoint) { if (!endpoint.EdgeID) { $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); @@ -121,6 +137,26 @@ angular }); } + function switchToKubernetesEdgeEndpoint(endpoint) { + if (!endpoint.EdgeID) { + $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); + return; + } + + EndpointProvider.setEndpointID(endpoint.Id); + $scope.state.connectingToEdgeEndpoint = true; + KubernetesHealthService.ping() + .then(function success() { + endpoint.Status = 1; + }) + .catch(function error() { + endpoint.Status = 2; + }) + .finally(function final() { + switchToKubernetesEndpoint(endpoint); + }); + } + function switchToDockerEndpoint(endpoint) { if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) { $scope.state.connectingToEdgeEndpoint = false; diff --git a/app/portainer/views/init/endpoint/includes/agent.html b/app/portainer/views/init/endpoint/includes/agent.html new file mode 100644 index 000000000..535355cdd --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/agent.html @@ -0,0 +1,34 @@ +

+ Information +
+
+
+ +

+ Connect directly to a Portainer agent running inside a Docker or Kubernetes environment. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/includes/azure.html b/app/portainer/views/init/endpoint/includes/azure.html new file mode 100644 index 000000000..dca7e4953 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/azure.html @@ -0,0 +1,63 @@ +
+ Information +
+
+
+ +

This feature is experimental.

+

+ Connect to Microsoft Azure to manage Azure Container Instances (ACI). +

+

+ + Have a look at + the Azure documentation + to retrieve the credentials required below. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ +
+ Azure credentials +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/includes/localDocker.html b/app/portainer/views/init/endpoint/includes/localDocker.html new file mode 100644 index 000000000..aea934557 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/localDocker.html @@ -0,0 +1,21 @@ +
+ Information +
+
+
+ +

+ Manage the Docker environment where Portainer is running. +

+

+ + Ensure that you have started the Portainer container with the following Docker flag: +

+

-v "/var/run/docker.sock:/var/run/docker.sock" (Linux).

+

+ or +

+

-v \\.\pipe\docker_engine:\\.\pipe\docker_engine (Windows).

+
+
+
diff --git a/app/portainer/views/init/endpoint/includes/localKubernetes.html b/app/portainer/views/init/endpoint/includes/localKubernetes.html new file mode 100644 index 000000000..8c2204f56 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/localKubernetes.html @@ -0,0 +1,12 @@ +
+ Information +
+
+
+ +

+ Manage the Kubernetes environment where Portainer is running. +

+
+
+
diff --git a/app/portainer/views/init/endpoint/includes/remote.html b/app/portainer/views/init/endpoint/includes/remote.html new file mode 100644 index 000000000..a6e23ab15 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/remote.html @@ -0,0 +1,120 @@ +
+ Information +
+
+
+ +

+ Connect Portainer to a remote Docker environment using the Docker API over TCP. +

+

+ + The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP + in the Docker documentation. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Required TLS files +
+ +
+ +
+ + + {{ ctrl.formValues.TLSCACert.name }} + + + +
+
+ +
+ +
+ +
+ + + {{ ctrl.formValues.TLSCert.name }} + + + +
+
+ + +
+ +
+ + + {{ ctrl.formValues.TLSKey.name }} + + + +
+
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index c67f678b6..3f3c50f99 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -4,8 +4,8 @@
- - + +
@@ -17,7 +17,7 @@
- Connect Portainer to the Docker environment you want to manage. + Connect Portainer to the container environment you want to manage.
@@ -25,377 +25,52 @@
-
- -
- -
-
- Information -
-
-
- -

- Manage the Docker environment where Portainer is running. -

-

- - Ensure that you have started the Portainer container with the following Docker flag: -

-

-v "/var/run/docker.sock:/var/run/docker.sock" (Linux).

-

- or -

-

-v \\.\pipe\docker_engine:\\.\pipe\docker_engine (Windows).

-
-
-
- -
-
- -
-
- + +
+
- - -
-
- Information -
-
-
- -

- Connect directly to a Portainer agent running inside a Swarm cluster. -

-

- - If you have started Portainer in the same overlay network as the agent, you can use tasks.AGENT_SERVICE_NAME:AGENT_SERVICE_PORT as the endpoint - URL format. -

-
-
-
-
- Environment -
- -
- -
- -
-
- - -
- -
- -
-
- - -
-
- -
-
- +
+
- - -
-
- Information -
-
-
- -

This feature is experimental.

-

- Connect to Microsoft Azure to manage Azure Container Instances (ACI). -

-

- - Have a look at - the Azure documentation - to retrieve the credentials required below. -

-
-
-
-
- Environment -
- -
- -
- -
-
- -
- Azure credentials -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
-
- -
-
- +
+
- - -
-
- Information -
-
-
- -

- Connect Portainer to a remote Docker environment using the Docker API over TCP. -

-

- - The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP - in the Docker documentation. -

-
-
-
-
- Environment -
- -
- -
- -
-
- - -
- -
- -
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
- - -
-
- - -
-
- -
- Required TLS files -
- -
- -
- - - {{ formValues.TLSCACert.name }} - - - -
-
- -
- -
- -
- - - {{ formValues.TLSCert.name }} - - - -
-
- - -
- -
- - - {{ formValues.TLSKey.name }} - - - -
-
- -
-
- - -
-
- -
-
- +
+
- +
+ +
+ + +
+
+ +
+
+
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index fc4ff905a..ca2d94ca3 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -1,109 +1,223 @@ import _ from 'lodash-es'; +import angular from 'angular'; +import { PortainerEndpointInitFormValues, PortainerEndpointInitFormValueEndpointSections } from 'Portainer/models/endpoint/formValues'; +import { PortainerEndpointTypes, PortainerEndpointConnectionTypes } from 'Portainer/models/endpoint/models'; -angular.module('portainer.app').controller('InitEndpointController', [ - '$scope', - '$state', - 'EndpointService', - 'StateManager', - 'Notifications', - function ($scope, $state, EndpointService, StateManager, Notifications) { - if (!_.isEmpty($scope.applicationState.endpoint)) { - $state.go('portainer.home'); +require('./includes/localDocker.html'); +require('./includes/localKubernetes.html'); +require('./includes/remote.html'); +require('./includes/azure.html'); +require('./includes/agent.html'); + +class InitEndpointController { + /* @ngInject */ + constructor($async, $scope, $state, EndpointService, EndpointProvider, StateManager, Notifications) { + this.$async = $async; + this.$scope = $scope; + this.$state = $state; + this.EndpointService = EndpointService; + this.EndpointProvider = EndpointProvider; + this.StateManager = StateManager; + this.Notifications = Notifications; + + this.createLocalEndpointAsync = this.createLocalEndpointAsync.bind(this); + this.createLocalKubernetesEndpointAsync = this.createLocalKubernetesEndpointAsync.bind(this); + this.createAgentEndpointAsync = this.createAgentEndpointAsync.bind(this); + this.createAzureEndpointAsync = this.createAzureEndpointAsync.bind(this); + this.createRemoteEndpointAsync = this.createRemoteEndpointAsync.bind(this); + } + + $onInit() { + if (!_.isEmpty(this.$scope.applicationState.endpoint)) { + this.$state.go('portainer.home'); } + this.logo = this.StateManager.getState().application.logo; - $scope.logo = StateManager.getState().application.logo; - - $scope.state = { + this.state = { uploadInProgress: false, actionInProgress: false, }; - $scope.formValues = { - EndpointType: 'remote', - Name: '', - URL: '', - TLS: false, - TLSSkipVerify: false, - TLSSKipClientVerify: false, - TLSCACert: null, - TLSCert: null, - TLSKey: null, - AzureApplicationId: '', - AzureTenantId: '', - AzureAuthenticationKey: '', - }; + this.formValues = new PortainerEndpointInitFormValues(); + this.endpointSections = PortainerEndpointInitFormValueEndpointSections; + this.PortainerEndpointConnectionTypes = PortainerEndpointConnectionTypes; + } - $scope.createLocalEndpoint = function () { - $scope.state.actionInProgress = true; - EndpointService.createLocalEndpoint() - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; + isRemoteConnectButtonDisabled() { + return ( + this.state.actionInProgress || + !this.formValues.Name || + !this.formValues.URL || + (this.formValues.TLS && + ((this.formValues.TLSVerify && !this.formValues.TLSCACert) || (!this.formValues.TLSSKipClientVerify && (!this.formValues.TLSCert || !this.formValues.TLSKey)))) + ); + } - $scope.createAzureEndpoint = function () { - var name = $scope.formValues.Name; - var applicationId = $scope.formValues.AzureApplicationId; - var tenantId = $scope.formValues.AzureTenantId; - var authenticationKey = $scope.formValues.AzureAuthenticationKey; + isAzureConnectButtonDisabled() { + return this.state.actionInProgress || !this.formValues.Name || !this.formValues.AzureApplicationId || !this.formValues.AzureTenantId || !this.formValues.AzureAuthenticationKey; + } - createAzureEndpoint(name, applicationId, tenantId, authenticationKey); - }; - - $scope.createAgentEndpoint = function () { - var name = $scope.formValues.Name; - var URL = $scope.formValues.URL; - var PublicURL = URL.split(':')[0]; - - createRemoteEndpoint(name, 2, URL, PublicURL, true, true, true, null, null, null); - }; - - $scope.createRemoteEndpoint = function () { - var name = $scope.formValues.Name; - var URL = $scope.formValues.URL; - var PublicURL = URL.split(':')[0]; - var TLS = $scope.formValues.TLS; - var TLSSkipVerify = TLS && $scope.formValues.TLSSkipVerify; - var TLSSKipClientVerify = TLS && $scope.formValues.TLSSKipClientVerify; - var TLSCAFile = TLSSkipVerify ? null : $scope.formValues.TLSCACert; - var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert; - var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey; - - createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); - }; - - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) { - $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); + isConnectButtonDisabled() { + switch (this.formValues.ConnectionType) { + case PortainerEndpointConnectionTypes.DOCKER_LOCAL: + return this.state.actionInProgress; + case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: + return this.state.actionInProgress; + case PortainerEndpointConnectionTypes.REMOTE: + return this.isRemoteConnectButtonDisabled(); + case PortainerEndpointConnectionTypes.AZURE: + return this.isAzureConnectButtonDisabled(); + case PortainerEndpointConnectionTypes.AGENT: + return this.state.actionInProgress || !this.formValues.Name || !this.formValues.URL; + default: + break; } + } - function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { - $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); + createEndpoint() { + switch (this.formValues.ConnectionType) { + case PortainerEndpointConnectionTypes.DOCKER_LOCAL: + return this.createLocalEndpoint(); + case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: + return this.createLocalKubernetesEndpoint(); + case PortainerEndpointConnectionTypes.REMOTE: + return this.createRemoteEndpoint(); + case PortainerEndpointConnectionTypes.AZURE: + return this.createAzureEndpoint(); + case PortainerEndpointConnectionTypes.AGENT: + return this.createAgentEndpoint(); + default: + this.Notifications.error('Failure', 'Unable to determine wich action to do'); } - }, -]); + } + + /** + * DOCKER_LOCAL (1) + */ + async createLocalEndpointAsync() { + try { + this.state.actionInProgress = true; + await this.EndpointService.createLocalEndpoint(); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createLocalEndpoint() { + return this.$async(this.createLocalEndpointAsync); + } + + /** + * KUBERNETES_LOCAL (5) + */ + async createLocalKubernetesEndpointAsync() { + try { + this.state.actionInProgress = true; + const endpoint = await this.EndpointService.createLocalKubernetesEndpoint(); + this.$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Kubernetes environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createLocalKubernetesEndpoint() { + return this.$async(this.createLocalKubernetesEndpointAsync); + } + + /** + * DOCKER / KUBERNETES AGENT (2 / 6) + */ + async createAgentEndpointAsync() { + try { + this.state.actionInProgress = true; + const name = this.formValues.Name; + const URL = this.formValues.URL; + const PublicURL = URL.split(':')[0]; + // TODO: k8s merge - change type ID for agent on kube (6) or agent on swarm (2) + const endpoint = await this.EndpointService.createRemoteEndpoint( + name, + PortainerEndpointTypes.AgentOnKubernetesEnvironment, + URL, + PublicURL, + 1, + [], + true, + true, + true, + null, + null, + null + ); + // TODO: k8s merge - go on home whith agent on swarm (2) + this.$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createAgentEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } + + /** + * DOCKER REMOTE (1) + */ + async createRemoteEndpointAsync() { + try { + this.state.actionInProgress = true; + const name = this.formValues.Name; + const type = PortainerEndpointTypes.DockerEnvironment; + const URL = this.formValues.URL; + const PublicURL = URL.split(':')[0]; + const TLS = this.formValues.TLS; + const TLSSkipVerify = TLS && this.formValues.TLSSkipVerify; + const TLSSKipClientVerify = TLS && this.formValues.TLSSKipClientVerify; + const TLSCAFile = TLSSkipVerify ? null : this.formValues.TLSCACert; + const TLSCertFile = TLSSKipClientVerify ? null : this.formValues.TLSCert; + const TLSKeyFile = TLSSKipClientVerify ? null : this.formValues.TLSKey; + await this.EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createRemoteEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } + + /** + * AZURE (4) + */ + async createAzureEndpointAsync() { + try { + this.state.actionInProgress = true; + var name = this.formValues.Name; + var applicationId = this.formValues.AzureApplicationId; + var tenantId = this.formValues.AzureTenantId; + var authenticationKey = this.formValues.AzureAuthenticationKey; + await this.EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createAzureEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } +} + +export default InitEndpointController; +angular.module('portainer.app').controller('InitEndpointController', InitEndpointController); diff --git a/app/portainer/views/logout/logout.html b/app/portainer/views/logout/logout.html new file mode 100644 index 000000000..60311e9da --- /dev/null +++ b/app/portainer/views/logout/logout.html @@ -0,0 +1,16 @@ +
+ +
+
+ +
+ + +
+
+ Logout in progress... + +
+
+
+
diff --git a/app/portainer/views/logout/logoutController.js b/app/portainer/views/logout/logoutController.js new file mode 100644 index 000000000..21d51bc84 --- /dev/null +++ b/app/portainer/views/logout/logoutController.js @@ -0,0 +1,61 @@ +import angular from 'angular'; + +class LogoutController { + /* @ngInject */ + constructor($async, $state, $transition$, Authentication, StateManager, Notifications, LocalStorage) { + this.$async = $async; + this.$state = $state; + this.$transition$ = $transition$; + + this.Authentication = Authentication; + this.StateManager = StateManager; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + + this.logo = this.StateManager.getState().application.logo; + this.logoutAsync = this.logoutAsync.bind(this); + + this.onInit = this.onInit.bind(this); + } + + /** + * UTILS FUNCTIONS SECTION + */ + async logoutAsync() { + const error = this.$transition$.params().error; + const performApiLogout = this.$transition$.params().performApiLogout; + try { + await this.Authentication.logout(performApiLogout); + } finally { + this.LocalStorage.storeLogoutReason(error); + this.$state.go('portainer.auth', { reload: true }); + } + } + + logout() { + return this.$async(this.logoutAsync); + } + + /** + * END UTILS FUNCTIONS SECTION + */ + + async onInit() { + try { + await this.logout(); + } catch (err) { + this.Notifications.error('Failure', err, 'An error occured during logout'); + } + } + + $onInit() { + return this.$async(this.onInit); + } + + /** + * END ON INIT SECTION + */ +} + +export default LogoutController; +angular.module('portainer.app').controller('LogoutController', LogoutController); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 18f8d5df9..a8a5ea1de 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -13,9 +13,11 @@ Home + +
- +
@@ -123,7 +123,7 @@ >
- +
diff --git a/app/vendors.js b/app/vendors.js index f4d8ce6a1..db03b38b4 100644 --- a/app/vendors.js +++ b/app/vendors.js @@ -12,9 +12,9 @@ import 'angular-json-tree/dist/angular-json-tree.css'; import 'angular-loading-bar/build/loading-bar.css'; import 'angular-moment-picker/dist/angular-moment-picker.min.css'; import 'angular-multiselect/isteven-multi-select.css'; +import 'spinkit/spinkit.min.css'; import angular from 'angular'; -window.angular = angular; import 'moment'; import '@uirouter/angularjs'; import 'ui-select'; @@ -38,3 +38,5 @@ import 'js-yaml/dist/js-yaml.js'; import 'angular-ui-bootstrap'; import 'angular-moment-picker'; import 'angular-multiselect/isteven-multi-select.js'; + +window.angular = angular; diff --git a/build/download_kompose_binary.sh b/build/download_kompose_binary.sh new file mode 100755 index 000000000..b5021e70d --- /dev/null +++ b/build/download_kompose_binary.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +PLATFORM=$1 +ARCH=$2 +KOMPOSE_VERSION=$3 + +wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}" +chmod +x "dist/kompose" + +exit 0 diff --git a/build/download_kubectl_binary.sh b/build/download_kubectl_binary.sh new file mode 100755 index 000000000..b9dd72f73 --- /dev/null +++ b/build/download_kubectl_binary.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +PLATFORM=$1 +ARCH=$2 +KUBECTL_VERSION=$3 + +wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl" +chmod +x "dist/kubectl" + +exit 0 diff --git a/gruntfile.js b/gruntfile.js index bd7855302..919fc6d93 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -16,8 +16,12 @@ module.exports = function (grunt) { grunt.initConfig({ root: 'dist', distdir: 'dist/public', - shippedDockerVersion: '18.09.3', - shippedDockerVersionWindows: '17.09.0-ce', + binaries: { + dockerLinuxVersion: '18.09.3', + dockerWindowsVersion: '17.09.0-ce', + komposeVersion: 'v1.21.0', + kubectlVersion: 'v1.18.0', + }, config: gruntfile_cfg.config, env: gruntfile_cfg.env, src: gruntfile_cfg.src, @@ -30,7 +34,12 @@ module.exports = function (grunt) { grunt.registerTask('lint', ['eslint']); - grunt.registerTask('build:server', ['shell:build_binary:linux:' + arch, 'shell:download_docker_binary:linux:' + arch]); + grunt.registerTask('build:server', [ + 'shell:build_binary:linux:' + arch, + 'shell:download_docker_binary:linux:' + arch, + 'shell:download_kompose_binary:linux:' + arch, + 'shell:download_kubectl_binary:linux:' + arch, + ]); grunt.registerTask('build:client', ['config:dev', 'env:dev', 'webpack:dev']); @@ -47,7 +56,17 @@ module.exports = function (grunt) { grunt.registerTask('start:toolkit', ['start:localserver', 'start:client']); grunt.task.registerTask('release', 'release::', function (p = 'linux', a = arch) { - grunt.task.run(['config:prod', 'env:prod', 'clean:all', 'copy:assets', 'shell:build_binary:' + p + ':' + a, 'shell:download_docker_binary:' + p + ':' + a, 'webpack:prod']); + grunt.task.run([ + 'config:prod', + 'env:prod', + 'clean:all', + 'copy:assets', + 'shell:build_binary:' + p + ':' + a, + 'shell:download_docker_binary:' + p + ':' + a, + 'shell:download_kompose_binary:' + p + ':' + a, + 'shell:download_kubectl_binary:' + p + ':' + a, + 'webpack:prod', + ]); }); grunt.task.registerTask('devopsbuild', 'devopsbuild::', function (p, a) { @@ -58,6 +77,8 @@ module.exports = function (grunt) { 'copy:assets', 'shell:build_binary_azuredevops:' + p + ':' + a, 'shell:download_docker_binary:' + p + ':' + a, + 'shell:download_kompose_binary:' + p + ':' + a, + 'shell:download_kubectl_binary:' + p + ':' + a, 'webpack:prod', ]); }); @@ -97,7 +118,6 @@ gruntfile_cfg.src = { gruntfile_cfg.clean = { server: ['<%= root %>/portainer'], client: ['<%= distdir %>/*'], - docker: ['<%= root %>/docker'], all: ['<%= root %>/*'], }; @@ -122,6 +142,8 @@ gruntfile_cfg.shell = { build_binary: { command: shell_build_binary }, build_binary_azuredevops: { command: shell_build_binary_azuredevops }, download_docker_binary: { command: shell_download_docker_binary }, + download_kompose_binary: { command: shell_download_kompose_binary }, + download_kubectl_binary: { command: shell_download_kubectl_binary }, run_container: { command: shell_run_container }, run_localserver: { command: shell_run_localserver, options: { async: true } }, install_yarndeps: { command: shell_install_yarndeps }, @@ -172,9 +194,10 @@ function shell_download_docker_binary(p, a) { var as = { amd64: 'x86_64', arm: 'armhf', arm64: 'aarch64' }; var ip = ps[p] === undefined ? p : ps[p]; var ia = as[a] === undefined ? a : as[a]; - var binaryVersion = p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>'; + var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>'; + if (p === 'linux' || p === 'mac') { - return ['if [ -f dist/docker ]; then', 'echo "Docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' '); + return ['if [ -f dist/docker ]; then', 'echo "docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' '); } else { return [ 'powershell -Command "& {if (Get-Item -Path dist/docker.exe -ErrorAction:SilentlyContinue) {', @@ -185,3 +208,35 @@ function shell_download_docker_binary(p, a) { ].join(' '); } } + +function shell_download_kompose_binary(p, a) { + var binaryVersion = '<%= binaries.komposeVersion %>'; + + if (p === 'linux' || p === 'darwin') { + return ['if [ -f dist/kompose ]; then', 'echo "kompose binary exists";', 'else', 'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path dist/kompose.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Docker binary exists"', + '} else {', + '& ".\\build\\download_kompose_binary.ps1" -docker_version ' + binaryVersion + '', + '}}"', + ].join(' '); + } +} + +function shell_download_kubectl_binary(p, a) { + var binaryVersion = '<%= binaries.kubectlVersion %>'; + + if (p === 'linux' || p === 'darwin') { + return ['if [ -f dist/kubectl ]; then', 'echo "kubectl binary exists";', 'else', 'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path dist/kubectl.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Docker binary exists"', + '} else {', + '& ".\\build\\download_kubectl_binary.ps1" -docker_version ' + binaryVersion + '', + '}}"', + ].join(' '); + } +} diff --git a/jsconfig.json b/jsconfig.json index e78006415..c1b062209 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -8,6 +8,7 @@ "Azure/*": ["azure/*"], "Docker/*": ["docker/*"], "Extensions/*": ["extensions/*"], + "Kubernetes/*": ["kubernetes/*"], "Portainer/*": ["portainer/*"] } }, diff --git a/package.json b/package.json index f663fd108..293fd6c14 100644 --- a/package.json +++ b/package.json @@ -77,17 +77,22 @@ "bootstrap": "^3.4.0", "chart.js": "~2.6.0", "codemirror": "~5.30.0", + "fast-json-patch": "^3.0.0-1", "filesize": "~3.3.0", + "filesize-parser": "^1.5.0", "jquery": "^3.5.1", "js-yaml": "^3.14.0", "lodash-es": "^4.17.15", "moment": "^2.21.0", "ng-file-upload": "~12.2.13", + "spinkit": "^2.0.1", "splitargs": "github:deviantony/splitargs#semver:~0.2.0", + "strip-ansi": "^6.0.0", "toastr": "^2.1.4", "ui-select": "^0.19.8", "uuid": "^3.3.2", - "xterm": "^3.8.0" + "xterm": "^3.8.0", + "yaml": "^1.10.0" }, "devDependencies": { "@babel/core": "^7.1.2", @@ -158,4 +163,4 @@ "*.js": "eslint --cache --fix", "*.{js,css,md,html}": "prettier --write" } -} +} \ No newline at end of file diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 3603afe87..f0258a201 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -114,6 +114,7 @@ module.exports = { Agent: path.resolve(projectRoot, 'app/agent'), Azure: path.resolve(projectRoot, 'app/azure'), Docker: path.resolve(projectRoot, 'app/docker'), + Kubernetes: path.resolve(projectRoot, 'app/kubernetes'), Extensions: path.resolve(projectRoot, 'app/extensions'), Portainer: path.resolve(projectRoot, 'app/portainer'), '@': path.resolve(projectRoot, 'app'), diff --git a/yarn.lock b/yarn.lock index d46f49040..77b495a16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,35 +2,35 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" - integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.3.tgz#324bcfd8d35cd3d47dae18cde63d752086435e9a" + integrity sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg== dependencies: - "@babel/highlight" "^7.8.3" + "@babel/highlight" "^7.10.3" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== +"@babel/compat-data@^7.10.1", "@babel/compat-data@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.3.tgz#9af3e033f36e8e2d6e47570db91e64a846f5d382" + integrity sha512-BDIfJ9uNZuI0LajPfoYV28lX8kyCPMHY6uY4WH1lJdcicmAfxCK5ASzaeV0D/wsUaRH/cLk+amuxtC37sZ8TUg== dependencies: - browserslist "^4.9.1" + browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" "@babel/core@^7.1.2": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.3.tgz#73b0e8ddeec1e3fdd7a2de587a60e17c440ec77e" + integrity sha512-5YqWxYE3pyhIi84L84YcwjeEgS+fa7ZjK6IBVGTjDVfm64njkR2lfDhVR5OudLk8x2GK59YoSyVv+L/03k1q9w== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/code-frame" "^7.10.3" + "@babel/generator" "^7.10.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helpers" "^7.10.1" + "@babel/parser" "^7.10.3" + "@babel/template" "^7.10.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,283 +40,312 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" - integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== +"@babel/generator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.3.tgz#32b9a0d963a71d7a54f5f6c15659c3dbc2a523a5" + integrity sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA== dependencies: - "@babel/types" "^7.9.0" + "@babel/types" "^7.10.3" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== +"@babel/helper-annotate-as-pure@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268" + integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.1" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503" - integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.1": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.3.tgz#4e9012d6701bef0030348d7f9c808209bd3e8687" + integrity sha512-lo4XXRnBlU6eRM92FkiZxpo1xFLmv3VsPFk61zJKMm7XYJfwqXHsYJTY6agoc4a3L8QPw1HqWehO18coZgbT6A== dependencies: - "@babel/helper-explode-assignable-expression" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-explode-assignable-expression" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== +"@babel/helper-compilation-targets@^7.10.2": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz#a17d9723b6e2c750299d2a14d4637c76936d8285" + integrity sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA== dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" + "@babel/compat-data" "^7.10.1" + browserslist "^4.12.0" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== +"@babel/helper-create-class-features-plugin@^7.10.1": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz#2783daa6866822e3d5ed119163b50f0fc3ae4b35" + integrity sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-member-expression-to-functions" "^7.10.3" + "@babel/helper-optimise-call-expression" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + +"@babel/helper-create-regexp-features-plugin@^7.10.1", "@babel/helper-create-regexp-features-plugin@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd" + integrity sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-regex" "^7.10.1" regexpu-core "^4.7.0" -"@babel/helper-define-map@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" - integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g== +"@babel/helper-define-map@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.3.tgz#d27120a5e57c84727b30944549b2dfeca62401a8" + integrity sha512-bxRzDi4Sin/k0drWCczppOhov1sBSdBvXJObM1NLHQzjhXhwRtn7aRWGvLJWCYbuu2qUk3EKs6Ci9C9ps8XokQ== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/types" "^7.10.3" lodash "^4.17.13" -"@babel/helper-explode-assignable-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982" - integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw== +"@babel/helper-explode-assignable-expression@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.3.tgz#9dc14f0cfa2833ea830a9c8a1c742b6e7461b05e" + integrity sha512-0nKcR64XrOC3lsl+uhD15cwxPvaB6QKUDlD84OT9C3myRbhJqTMYir69/RWItUvHpharv0eJ/wk7fl34ONSwZw== dependencies: - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" - integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== +"@babel/helper-function-name@^7.10.1", "@babel/helper-function-name@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz#79316cd75a9fa25ba9787ff54544307ed444f197" + integrity sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw== dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-get-function-arity" "^7.10.3" + "@babel/template" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== +"@babel/helper-get-function-arity@^7.10.1", "@babel/helper-get-function-arity@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz#3a28f7b28ccc7719eacd9223b659fdf162e4c45e" + integrity sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-hoist-variables@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134" - integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg== +"@babel/helper-hoist-variables@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.3.tgz#d554f52baf1657ffbd7e5137311abc993bb3f068" + integrity sha512-9JyafKoBt5h20Yv1+BXQMdcXXavozI1vt401KBiRc2qzUepbVnd7ogVNymY1xkQN9fekGwfxtotH2Yf5xsGzgg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== +"@babel/helper-member-expression-to-functions@^7.10.1", "@babel/helper-member-expression-to-functions@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz#bc3663ac81ac57c39148fef4c69bf48a77ba8dd6" + integrity sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== +"@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.10.1", "@babel/helper-module-imports@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz#766fa1d57608e53e5676f23ae498ec7a95e1b11a" + integrity sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== +"@babel/helper-module-transforms@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622" + integrity sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg== dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" + "@babel/helper-module-imports" "^7.10.1" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-simple-access" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/template" "^7.10.1" + "@babel/types" "^7.10.1" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== +"@babel/helper-optimise-call-expression@^7.10.1", "@babel/helper-optimise-call-expression@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz#f53c4b6783093195b0f69330439908841660c530" + integrity sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.10.3", "@babel/helper-plugin-utils@^7.8.0": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz#aac45cccf8bc1873b99a85f34bceef3beb5d3244" + integrity sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g== -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== +"@babel/helper-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96" + integrity sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g== dependencies: lodash "^4.17.13" -"@babel/helper-remap-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86" - integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA== +"@babel/helper-remap-async-to-generator@^7.10.1", "@babel/helper-remap-async-to-generator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.3.tgz#18564f8a6748be466970195b876e8bba3bccf442" + integrity sha512-sLB7666ARbJUGDO60ZormmhQOyqMX/shKBXZ7fy937s+3ID8gSrneMvKSSb+8xIM5V7Vn6uNVtOY1vIm26XLtA== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-wrap-function" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-wrap-function" "^7.10.1" + "@babel/template" "^7.10.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== +"@babel/helper-replace-supers@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" + integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A== dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/helper-member-expression-to-functions" "^7.10.1" + "@babel/helper-optimise-call-expression" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== +"@babel/helper-simple-access@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" + integrity sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw== dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/template" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== +"@babel/helper-split-export-declaration@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" + integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.1" -"@babel/helper-validator-identifier@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" - integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== +"@babel/helper-validator-identifier@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" + integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== -"@babel/helper-wrap-function@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" - integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ== +"@babel/helper-wrap-function@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9" + integrity sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-function-name" "^7.10.1" + "@babel/template" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== +"@babel/helpers@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" + integrity sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw== dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/template" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== +"@babel/highlight@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.3.tgz#c633bb34adf07c5c13156692f5922c81ec53f28d" + integrity sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw== dependencies: - "@babel/helper-validator-identifier" "^7.9.0" + "@babel/helper-validator-identifier" "^7.10.3" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" + integrity sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA== -"@babel/plugin-proposal-async-generator-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" - integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== +"@babel/plugin-proposal-async-generator-functions@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.3.tgz#5a02453d46e5362e2073c7278beab2e53ad7d939" + integrity sha512-WUUWM7YTOudF4jZBAJIW9D7aViYC/Fn0Pln4RIHlQALyno3sXSjqmTA4Zy1TKC2D49RCR8Y/Pn4OIUtEypK3CA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-remap-async-to-generator" "^7.10.3" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" - integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== +"@babel/plugin-proposal-class-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01" + integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-proposal-dynamic-import@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0" + integrity sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" - integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== +"@babel/plugin-proposal-json-strings@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09" + integrity sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2" - integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78" + integrity sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8" - integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ== +"@babel/plugin-proposal-numeric-separator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123" + integrity sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-syntax-numeric-separator" "^7.10.1" -"@babel/plugin-proposal-object-rest-spread@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== +"@babel/plugin-proposal-object-rest-spread@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz#b8d0d22f70afa34ad84b7a200ff772f9b9fce474" + integrity sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.10.1" -"@babel/plugin-proposal-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" - integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== +"@babel/plugin-proposal-optional-catch-binding@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz#c9f86d99305f9fa531b568ff5ab8c964b8b223d2" + integrity sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" - integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== +"@babel/plugin-proposal-optional-chaining@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.3.tgz#9a726f94622b653c0a3a7a59cdce94730f526f7c" + integrity sha512-yyG3n9dJ1vZ6v5sfmIlMMZ8azQoqx/5/nZTSWX1td6L1H1bsjzA8TInDChpafCZiJkeOFzp/PtrfigAQXxI1Ng== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== +"@babel/plugin-proposal-private-methods@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598" + integrity sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-proposal-unicode-property-regex@^7.10.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f" + integrity sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-async-generators@^7.8.0": version "7.8.4" @@ -325,6 +354,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-class-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5" + integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -346,12 +382,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== +"@babel/plugin-syntax-numeric-separator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99" + integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-object-rest-spread@^7.8.0": version "7.8.3" @@ -374,326 +410,337 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" - integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== +"@babel/plugin-syntax-top-level-await@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz#8b8733f8c57397b3eaa47ddba8841586dcaef362" + integrity sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-arrow-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" - integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== +"@babel/plugin-transform-arrow-functions@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b" + integrity sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" - integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== +"@babel/plugin-transform-async-to-generator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz#e5153eb1a3e028f79194ed8a7a4bf55f862b2062" + integrity sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg== dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" + "@babel/helper-module-imports" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-remap-async-to-generator" "^7.10.1" -"@babel/plugin-transform-block-scoped-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" - integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== +"@babel/plugin-transform-block-scoped-functions@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz#146856e756d54b20fff14b819456b3e01820b85d" + integrity sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-block-scoping@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" - integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== +"@babel/plugin-transform-block-scoping@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz#47092d89ca345811451cd0dc5d91605982705d5e" + integrity sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d" - integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ== +"@babel/plugin-transform-classes@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.3.tgz#8d9a656bc3d01f3ff69e1fccb354b0f9d72ac544" + integrity sha512-irEX0ChJLaZVC7FvvRoSIxJlmk0IczFLcwaRXUArBKYHCHbOhe57aG8q3uw/fJsoSXvZhjRX960hyeAGlVBXZw== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-define-map" "^7.8.3" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-define-map" "^7.10.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-optimise-call-expression" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" - integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== +"@babel/plugin-transform-computed-properties@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.3.tgz#d3aa6eef67cb967150f76faff20f0abbf553757b" + integrity sha512-GWzhaBOsdbjVFav96drOz7FzrcEW6AP5nax0gLIpstiFaI3LOb2tAg06TimaWU6YKOfUACK3FVrxPJ4GSc5TgA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" -"@babel/plugin-transform-destructuring@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b" - integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ== +"@babel/plugin-transform-destructuring@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz#abd58e51337815ca3a22a336b85f62b998e71907" + integrity sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== +"@babel/plugin-transform-dotall-regex@^7.10.1", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz#920b9fec2d78bb57ebb64a644d5c2ba67cc104ee" + integrity sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-duplicate-keys@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" - integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== +"@babel/plugin-transform-duplicate-keys@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz#c900a793beb096bc9d4d0a9d0cde19518ffc83b9" + integrity sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-exponentiation-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" - integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== +"@babel/plugin-transform-exponentiation-operator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz#279c3116756a60dd6e6f5e488ba7957db9c59eb3" + integrity sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-for-of@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" - integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== +"@babel/plugin-transform-for-of@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz#ff01119784eb0ee32258e8646157ba2501fcfda5" + integrity sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" - integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== +"@babel/plugin-transform-function-name@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz#4ed46fd6e1d8fde2a2ec7b03c66d853d2c92427d" + integrity sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-function-name" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" - integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== +"@babel/plugin-transform-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz#5794f8da82846b22e4e6631ea1658bce708eb46a" + integrity sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-member-expression-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" - integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== +"@babel/plugin-transform-member-expression-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz#90347cba31bca6f394b3f7bd95d2bbfd9fce2f39" + integrity sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== +"@babel/plugin-transform-modules-amd@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz#65950e8e05797ebd2fe532b96e19fc5482a1d52a" + integrity sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== +"@babel/plugin-transform-modules-commonjs@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301" + integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-simple-access" "^7.10.1" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== +"@babel/plugin-transform-modules-systemjs@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.3.tgz#004ae727b122b7b146b150d50cba5ffbff4ac56b" + integrity sha512-GWXWQMmE1GH4ALc7YXW56BTh/AlzvDWhUNn9ArFF0+Cz5G8esYlVbXfdyHa1xaD1j+GnBoCeoQNlwtZTVdiG/A== dependencies: - "@babel/helper-hoist-variables" "^7.8.3" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-hoist-variables" "^7.10.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.3" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697" - integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ== +"@babel/plugin-transform-modules-umd@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz#ea080911ffc6eb21840a5197a39ede4ee67b1595" + integrity sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" - integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.3.tgz#a4f8444d1c5a46f35834a410285f2c901c007ca6" + integrity sha512-I3EH+RMFyVi8Iy/LekQm948Z4Lz4yKT7rK+vuCAeRm0kTa6Z5W7xuhRxDNJv0FPya/her6AUgrDITb70YHtTvA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.8.3" -"@babel/plugin-transform-new-target@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" - integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== +"@babel/plugin-transform-new-target@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz#6ee41a5e648da7632e22b6fb54012e87f612f324" + integrity sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-object-super@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" - integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== +"@babel/plugin-transform-object-super@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz#2e3016b0adbf262983bf0d5121d676a5ed9c4fde" + integrity sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-replace-supers" "^7.10.1" -"@babel/plugin-transform-parameters@^7.8.7": - version "7.9.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a" - integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg== +"@babel/plugin-transform-parameters@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd" + integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg== dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-get-function-arity" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-property-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" - integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== +"@babel/plugin-transform-property-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz#cffc7315219230ed81dc53e4625bf86815b6050d" + integrity sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-regenerator@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8" - integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA== +"@babel/plugin-transform-regenerator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.3.tgz#6ec680f140a5ceefd291c221cb7131f6d7e8cb6d" + integrity sha512-H5kNeW0u8mbk0qa1jVIVTeJJL6/TJ81ltD4oyPx0P499DhMJrTmmIFCmJ3QloGpQG8K9symccB7S7SJpCKLwtw== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" - integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== +"@babel/plugin-transform-reserved-words@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz#0fc1027312b4d1c3276a57890c8ae3bcc0b64a86" + integrity sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-shorthand-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" - integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== +"@babel/plugin-transform-shorthand-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3" + integrity sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" - integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== +"@babel/plugin-transform-spread@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz#0c6d618a0c4461a274418460a28c9ccf5239a7c8" + integrity sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-sticky-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" - integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== +"@babel/plugin-transform-sticky-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz#90fc89b7526228bed9842cff3588270a7a393b00" + integrity sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-regex" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-regex" "^7.10.1" -"@babel/plugin-transform-template-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" - integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== +"@babel/plugin-transform-template-literals@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.3.tgz#69d39b3d44b31e7b4864173322565894ce939b25" + integrity sha512-yaBn9OpxQra/bk0/CaA4wr41O0/Whkg6nqjqApcinxM7pro51ojhX6fv1pimAnVjVfDy14K0ULoRL70CA9jWWA== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.3" -"@babel/plugin-transform-typeof-symbol@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" - integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== +"@babel/plugin-transform-typeof-symbol@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz#60c0239b69965d166b80a84de7315c1bc7e0bb0e" + integrity sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-unicode-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" - integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== +"@babel/plugin-transform-unicode-escapes@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940" + integrity sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-transform-unicode-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f" + integrity sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/polyfill@^7.2.5": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.8.7.tgz#151ec24c7135481336168c3bd8b8bf0cf91c032f" - integrity sha512-LeSfP9bNZH2UOZgcGcZ0PIHUt1ZuHub1L3CVmEyqLxCeDLm4C5Gi8jRH8ZX2PNpDhQCo0z6y/+DIs2JlliXW8w== + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.10.1.tgz#d56d4c8be8dd6ec4dce2649474e9b707089f739f" + integrity sha512-TviueJ4PBW5p48ra8IMtLXVkDucrlOZAIZ+EXqS3Ot4eukHbWiqcn7DcqpA1k5PcKtmJ4Xl9xwdv6yQvvcA+3g== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" "@babel/preset-env@^7.1.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8" - integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.3.tgz#3e58c9861bbd93b6a679987c7e4bd365c56c80c9" + integrity sha512-jHaSUgiewTmly88bJtMHbOd1bJf2ocYxb5BWKSDQIP5tmgFuS/n0gl+nhSrYDhT33m0vPxp+rP8oYYgPgMNQlg== dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" + "@babel/compat-data" "^7.10.3" + "@babel/helper-compilation-targets" "^7.10.2" + "@babel/helper-module-imports" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/plugin-proposal-async-generator-functions" "^7.10.3" + "@babel/plugin-proposal-class-properties" "^7.10.1" + "@babel/plugin-proposal-dynamic-import" "^7.10.1" + "@babel/plugin-proposal-json-strings" "^7.10.1" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.1" + "@babel/plugin-proposal-numeric-separator" "^7.10.1" + "@babel/plugin-proposal-object-rest-spread" "^7.10.3" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.1" + "@babel/plugin-proposal-optional-chaining" "^7.10.3" + "@babel/plugin-proposal-private-methods" "^7.10.1" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.1" "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.1" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.9.0" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.8.3" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" - "@babel/plugin-transform-modules-umd" "^7.9.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.8.7" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.7" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.10.1" + "@babel/plugin-transform-arrow-functions" "^7.10.1" + "@babel/plugin-transform-async-to-generator" "^7.10.1" + "@babel/plugin-transform-block-scoped-functions" "^7.10.1" + "@babel/plugin-transform-block-scoping" "^7.10.1" + "@babel/plugin-transform-classes" "^7.10.3" + "@babel/plugin-transform-computed-properties" "^7.10.3" + "@babel/plugin-transform-destructuring" "^7.10.1" + "@babel/plugin-transform-dotall-regex" "^7.10.1" + "@babel/plugin-transform-duplicate-keys" "^7.10.1" + "@babel/plugin-transform-exponentiation-operator" "^7.10.1" + "@babel/plugin-transform-for-of" "^7.10.1" + "@babel/plugin-transform-function-name" "^7.10.1" + "@babel/plugin-transform-literals" "^7.10.1" + "@babel/plugin-transform-member-expression-literals" "^7.10.1" + "@babel/plugin-transform-modules-amd" "^7.10.1" + "@babel/plugin-transform-modules-commonjs" "^7.10.1" + "@babel/plugin-transform-modules-systemjs" "^7.10.3" + "@babel/plugin-transform-modules-umd" "^7.10.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.3" + "@babel/plugin-transform-new-target" "^7.10.1" + "@babel/plugin-transform-object-super" "^7.10.1" + "@babel/plugin-transform-parameters" "^7.10.1" + "@babel/plugin-transform-property-literals" "^7.10.1" + "@babel/plugin-transform-regenerator" "^7.10.3" + "@babel/plugin-transform-reserved-words" "^7.10.1" + "@babel/plugin-transform-shorthand-properties" "^7.10.1" + "@babel/plugin-transform-spread" "^7.10.1" + "@babel/plugin-transform-sticky-regex" "^7.10.1" + "@babel/plugin-transform-template-literals" "^7.10.3" + "@babel/plugin-transform-typeof-symbol" "^7.10.1" + "@babel/plugin-transform-unicode-escapes" "^7.10.1" + "@babel/plugin-transform-unicode-regex" "^7.10.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.0" - browserslist "^4.9.1" + "@babel/types" "^7.10.3" + browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" @@ -711,57 +758,57 @@ esutils "^2.0.2" "@babel/runtime-corejs3@^7.9.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz#3511797ddf9a3d6f3ce46b99cc835184817eaa4e" - integrity sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" + integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== +"@babel/runtime@^7.8.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== +"@babel/template@^7.10.1", "@babel/template@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" + integrity sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/code-frame" "^7.10.3" + "@babel/parser" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" - integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== +"@babel/traverse@^7.10.1", "@babel/traverse@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.3.tgz#0b01731794aa7b77b214bcd96661f18281155d7e" + integrity sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/code-frame" "^7.10.3" + "@babel/generator" "^7.10.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/parser" "^7.10.3" + "@babel/types" "^7.10.3" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0-beta.49", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" - integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== +"@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.1", "@babel/types@^7.10.3", "@babel/types@^7.2.0", "@babel/types@^7.4.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.3.tgz#6535e3b79fea86a6b09e012ea8528f935099de8e" + integrity sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA== dependencies: - "@babel/helper-validator-identifier" "^7.9.0" + "@babel/helper-validator-identifier" "^7.10.3" lodash "^4.17.13" to-fast-properties "^2.0.0" "@fortawesome/fontawesome-free@^5.11.2": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" - integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6" + integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -784,13 +831,6 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" - integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg== - dependencies: - any-observable "^0.3.0" - "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -801,22 +841,16 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/fined@*": version "1.1.2" resolved "https://registry.yarnpkg.com/@types/fined/-/fined-1.1.2.tgz#05d2b9f93d144855c97c18c9675f424ed01400c4" integrity sha512-hzzTS+X9EqDhx4vwdch/DOZci/bfh5J6Nyz8lqvyfBg2ROx2fPafX+LpDfpVgSvQKj0EYkwTYpBO3z2etwbkOw== "@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" + integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== dependencies: - "@types/events" "*" "@types/minimatch" "*" "@types/node" "*" @@ -835,6 +869,16 @@ dependencies: "@types/node" "*" +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/liftoff@^2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@types/liftoff/-/liftoff-2.5.0.tgz#aa5f030ae0952d1b86225f3e9f27f6d5b69714aa" @@ -850,9 +894,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "12.12.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.31.tgz#d6b4f9645fee17f11319b508fb1001797425da51" - integrity sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg== + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== "@types/parse-json@^4.0.0": version "4.0.0" @@ -860,9 +904,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/q@^1.5.1": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" - integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" + integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== "@types/through@*": version "0.0.30" @@ -1085,7 +1129,7 @@ acorn@^5.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.7, acorn@^6.2.1: +acorn@^6.0.7, acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== @@ -1116,9 +1160,9 @@ ajv-keywords@^1.0.0: integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773" + integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw== ajv@^4.7.0: version "4.11.8" @@ -1128,10 +1172,10 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.9.1: - version "6.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" - integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.9.1: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1248,9 +1292,9 @@ angular-utils-pagination@~0.11.1: integrity sha1-7618iHm+swrT13cH+T49DvUfLGY= angular@1.x, angular@^1.3: - version "1.7.9" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.9.tgz#e52616e8701c17724c3c238cfe4f9446fd570bc4" - integrity sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.0.tgz#b1ec179887869215cab6dfd0df2e42caa65b1b51" + integrity sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg== angular@~1.5.0: version "1.5.11" @@ -1267,7 +1311,7 @@ angularjs-slider@^6.4.0: resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.7.0.tgz#eb2229311b81b79315a36e7b5eb700e128f50319" integrity sha512-Cizsuax65wN2Y+htmA3safE5ALOSCyWcKyWkziaO8vCVymi26bQQs6kKDhkYc8GFix/KE7Oc9gH3QLlTUgD38w== -ansi-colors@^3.0.0: +ansi-colors@^3.0.0, ansi-colors@^3.2.1: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== @@ -1277,12 +1321,12 @@ ansi-escapes@^1.1.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== @@ -1326,7 +1370,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1334,11 +1378,6 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -1347,6 +1386,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + applause@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/applause/-/applause-1.2.2.tgz#a8468579e81f67397bb5634c29953bedcd0f56c0" @@ -1362,9 +1409,9 @@ aproba@^1.1.1: integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== arch@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== + version "2.1.2" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== archive-type@^4.0.0: version "4.0.0" @@ -1420,7 +1467,7 @@ array-flatten@^2.1.0: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-includes@^3.0.3: +array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== @@ -1456,7 +1503,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.1: +array.prototype.flat@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== @@ -1501,6 +1548,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -1597,10 +1649,10 @@ babel-plugin-angularjs-annotate@^0.10.0: "@babel/types" "^7.2.0" simple-is "~0.2.0" -babel-plugin-dynamic-import-node@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== dependencies: object.assign "^4.1.0" @@ -1716,6 +1768,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1741,10 +1798,15 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" + integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== body-parser@1.19.0: version "1.19.0" @@ -1822,7 +1884,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1865,7 +1927,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0: +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= @@ -1874,17 +1936,19 @@ browserify-rsa@^4.0.0: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + version "4.2.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" + integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.2" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" browserify-zlib@^0.2.0: version "0.2.0" @@ -1909,15 +1973,15 @@ browserslist@^2.11.3: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" -browserslist@^4.8.3, browserslist@^4.9.1: - version "4.11.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad" - integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A== +browserslist@^4.12.0, browserslist@^4.8.5: + version "4.12.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" + integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== dependencies: - caniuse-lite "^1.0.30001035" - electron-to-chromium "^1.3.380" - node-releases "^1.1.52" - pkg-up "^3.1.0" + caniuse-lite "^1.0.30001043" + electron-to-chromium "^1.3.413" + node-releases "^1.1.53" + pkg-up "^2.0.0" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1967,9 +2031,9 @@ buffer@^4.3.0: isarray "^1.0.0" buffer@^5.2.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" - integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" @@ -2111,14 +2175,14 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30001037" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001037.tgz#359b13c5545fca8885da3d2a79f1a61eaef3cb93" - integrity sha512-TMs8GQUrZG0i+qCRxHS3zV5Ivlk1fFkBebtYdCeBgupGx1zX3vTFI4IEuqchlqUVmqZE3YKFeWFRxd+jUZ38yA== + version "1.0.30001088" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001088.tgz#13490ed989b20f4b199168371219a8ba657bce03" + integrity sha512-Sqkmd4oi1oiyOn2EkTauy1Bx1sVFv+drLtjkK6q4vTQpaI9wCjGGU/87MQXCiIh/SMU0FrdSnnA358yHkz8p/Q== -caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30001035: - version "1.0.30001037" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001037.tgz#cf666560b14f8dfa18abc235db1ef2699273af6e" - integrity sha512-qQP40FzWQ1i9RTjxppOUnpM8OwTBFL5DQbjoR9Az32EtM7YUZOw9orFO6rj1C+xWAGzz+X3bUe09Jf5Ep+zpuA== +caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30001043: + version "1.0.30001088" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz#23a6b9e192106107458528858f2c0e0dba0d9073" + integrity sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg== caw@^2.0.0, caw@^2.0.1: version "2.0.1" @@ -2130,15 +2194,6 @@ caw@^2.0.0, caw@^2.0.1: tunnel-agent "^0.6.0" url-to-options "^1.0.1" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2150,6 +2205,15 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -2158,6 +2222,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + change-case@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.1.0.tgz#0e611b7edc9952df2e8513b27b42de72647dd17e" @@ -2210,7 +2282,7 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" -chokidar@^2.0.2, chokidar@^2.1.8: +chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -2229,6 +2301,21 @@ chokidar@^2.0.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" + integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chokidar@~0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-0.6.3.tgz#e85968fa235f21773d388c617af085bf2104425a" @@ -2312,7 +2399,7 @@ cli-cursor@^1.0.1: dependencies: restore-cursor "^1.0.1" -cli-cursor@^2.0.0, cli-cursor@^2.1.0: +cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= @@ -2327,22 +2414,22 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" - integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5" + integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= +cli-truncate@2.1.0, cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" + slice-ansi "^3.0.0" + string-width "^4.2.0" cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" - integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== cliui@^5.0.0: version "5.0.0" @@ -2433,12 +2520,12 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3, color-name@^1.0.0: +color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2483,15 +2570,15 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.20.0, commander@~2.20.3: +commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== commander@~2.1.0: version "2.1.0" @@ -2515,7 +2602,7 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compare-versions@^3.5.1: +compare-versions@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== @@ -2648,11 +2735,11 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.6.2: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" - integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" + integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== dependencies: - browserslist "^4.8.3" + browserslist "^4.8.5" semver "7.0.0" core-js-pure@^3.0.0: @@ -2699,7 +2786,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-hash@^1.1.0, create-hash@^1.1.2: +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -2710,7 +2797,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -2722,7 +2809,16 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2733,19 +2829,10 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" - integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2854,9 +2941,9 @@ css-what@2.1: integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css-what@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" - integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== + version "3.3.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" + integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== cssesc@^3.0.0: version "3.0.0" @@ -2945,11 +3032,6 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== - dateformat@1.0.2-1.2.3: version "1.0.2-1.2.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.2-1.2.3.tgz#b0220c02de98617433b72851cf47de3df2cdbee9" @@ -2970,7 +3052,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3370,20 +3452,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.380: - version "1.3.384" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.384.tgz#ca1d5710a4c53168431f1cbef39c8a971b646bf8" - integrity sha512-9jGNF78o450ymPf63n7/j1HrRAD4xGTsDkKY2X6jtCAWaYgph2A9xQjwfwRpj+AovkARMO+JfZuVCFTdandD6w== +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.413: + version "1.3.481" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.481.tgz#0d59e72a0aaeb876b43fb1d6e84bf0dfc99617e8" + integrity sha512-q2PeCP2PQXSYadDo9uNY+uHXjdB9PcsUpCVoGlY8TZOPHGlXdevlqW9PkKeqCxn2QBkGB8b6AcMO++gh8X82bA== -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= - -elliptic@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== +elliptic@^6.0.0, elliptic@^6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -3425,33 +3502,31 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" - integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== +enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" + integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== dependencies: graceful-fs "^4.1.2" memory-fs "^0.5.0" tapable "^1.0.0" +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + dependencies: + ansi-colors "^3.2.1" + entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -3467,22 +3542,22 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: - version "1.17.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" - integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" + is-callable "^1.2.0" + is-regex "^1.1.0" object-inspect "^1.7.0" object-keys "^1.1.1" object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" es-to-primitive@^1.2.1: version "1.2.1" @@ -3599,16 +3674,16 @@ escope@^3.6.0: estraverse "^4.1.1" eslint-config-prettier@^6.10.1: - version "6.10.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz#129ef9ec575d5ddc0e269667bf09defcd898642a" - integrity sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ== + version "6.11.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" + integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== dependencies: get-stdin "^6.0.0" -eslint-import-resolver-node@^0.3.2: - version "0.3.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" - integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg== +eslint-import-resolver-node@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== dependencies: debug "^2.6.9" resolve "^1.13.1" @@ -3624,7 +3699,7 @@ eslint-loader@^2.1.2: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-module-utils@^2.4.1: +eslint-module-utils@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== @@ -3633,22 +3708,23 @@ eslint-module-utils@^2.4.1: pkg-dir "^2.0.0" eslint-plugin-import@^2.20.2: - version "2.20.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" - integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== + version "2.21.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz#8fef77475cc5510801bedc95f84b932f7f334a7c" + integrity sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA== dependencies: - array-includes "^3.0.3" - array.prototype.flat "^1.2.1" + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.1" + eslint-import-resolver-node "^0.3.3" + eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" - object.values "^1.1.0" + object.values "^1.1.1" read-pkg-up "^2.0.0" - resolve "^1.12.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" eslint-scope@^4.0.3: version "4.0.3" @@ -3666,9 +3742,9 @@ eslint-utils@^1.3.1: eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint@5.16.0: version "5.16.0" @@ -3786,11 +3862,11 @@ esprima@~3.1.0: integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= esquery@^1.0.0, esquery@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe" - integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q== + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== dependencies: - estraverse "^5.0.0" + estraverse "^5.1.0" esrecurse@^4.1.0: version "4.2.1" @@ -3804,10 +3880,10 @@ estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" - integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== estraverse@~0.0.4: version "0.0.4" @@ -3838,9 +3914,9 @@ eventemitter2@~0.4.13: integrity sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas= eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + version "4.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" + integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== events@^3.0.0: version "3.1.0" @@ -3912,10 +3988,10 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" - integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g== +execa@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" @@ -3924,7 +4000,6 @@ execa@^3.4.0: merge-stream "^2.0.0" npm-run-path "^4.0.0" onetime "^5.1.0" - p-finally "^2.0.0" signal-exit "^3.0.2" strip-final-newline "^2.0.0" @@ -4067,14 +4142,14 @@ extglob@^2.0.4: to-regex "^3.0.1" fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.0.3: - version "3.2.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" - integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4083,6 +4158,11 @@ fast-glob@^3.0.3: micromatch "^4.0.2" picomatch "^2.2.1" +fast-json-patch@^3.0.0-1: + version "3.0.0-1" + resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz#4c68f2e7acfbab6d29d1719c44be51899c93dabb" + integrity sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4099,9 +4179,9 @@ fastparse@^1.1.1, fastparse@^1.1.2: integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== fastq@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" - integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== dependencies: reusify "^1.0.4" @@ -4131,7 +4211,7 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figures@^1.3.5, figures@^1.7.0: +figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= @@ -4146,7 +4226,7 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: +figures@^3.0.0, figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== @@ -4243,6 +4323,11 @@ fileset@0.1.x: glob "3.x" minimatch "0.x" +filesize-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/filesize-parser/-/filesize-parser-1.5.0.tgz#97ad66d5b0d7154b2e8b1b4e83f526aed33c62f3" + integrity sha512-UTDpJB22VvozK7t31slU9WCAPSdcUWuwD7P7S6LBXswdgzUz+YhoziLOohknFcx0Kq5LWCAj4MEKY9q3zGq47Q== + filesize@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" @@ -4333,16 +4418,6 @@ find-versions@^3.0.0, find-versions@^3.2.0: dependencies: semver-regex "^2.0.0" -findup-sync@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - findup-sync@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" @@ -4353,6 +4428,16 @@ findup-sync@^2.0.0: micromatch "^3.0.4" resolve-dir "^1.0.1" +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -4396,9 +4481,9 @@ flat-cache@^2.0.1: write "1.0.3" flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatten@^1.0.2: version "1.0.3" @@ -4414,11 +4499,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" - integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ== - dependencies: - debug "^3.0.0" + version "1.12.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" + integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -4478,13 +4561,18 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== dependencies: bindings "^1.5.0" nan "^2.12.1" +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4596,7 +4684,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0: +glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -4655,13 +4743,6 @@ glob@~7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -4671,6 +4752,13 @@ global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + global-prefix@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" @@ -4770,9 +4858,9 @@ got@^8.3.1: url-to-options "^1.0.1" graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== graceful-fs@~1, graceful-fs@~1.2.0: version "1.2.3" @@ -4978,9 +5066,9 @@ gruntify-eslint@^3.1.0: eslint "^3.0.0" handle-thing@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" - integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== handlebars@1.0.x: version "1.0.12" @@ -4991,13 +5079,14 @@ handlebars@1.0.x: uglify-js "~2.3" handlebars@^4.4.3: - version "4.7.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" - integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: + minimist "^1.2.5" neo-async "^2.6.0" - optimist "^0.6.1" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" @@ -5079,12 +5168,13 @@ has@^1.0.1, has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" @@ -5244,10 +5334,10 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== http-proxy-middleware@0.19.1: version "0.19.1" @@ -5279,13 +5369,13 @@ human-signals@^1.1.1: integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== husky@>=4: - version "4.2.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e" - integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ== + version "4.2.5" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36" + integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ== dependencies: - chalk "^3.0.0" + chalk "^4.0.0" ci-info "^2.0.0" - compare-versions "^3.5.1" + compare-versions "^3.6.0" cosmiconfig "^6.0.0" find-versions "^3.2.0" opencollective-postinstall "^2.0.2" @@ -5334,9 +5424,9 @@ ignore@^4.0.6: integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== ignore@^5.1.1: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== image-webpack-loader@^4.5.0: version "4.6.0" @@ -5454,7 +5544,7 @@ import-lazy@^3.1.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ== -import-local@2.0.0, import-local@^2.0.0: +import-local@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== @@ -5474,11 +5564,6 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -5507,7 +5592,7 @@ inherits@1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js= -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5566,9 +5651,9 @@ inquirer@^6.2.2: through "^2.3.6" inquirer@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" - integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + version "7.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a" + integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ== dependencies: ansi-escapes "^4.2.1" chalk "^3.0.0" @@ -5592,10 +5677,10 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.0.0, interpret@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== +interpret@^1.0.0, interpret@^1.2.0, interpret@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== interpret@~1.1.0: version "1.1.0" @@ -5617,11 +5702,6 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5686,15 +5766,22 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" - integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== +is-callable@^1.1.4, is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== is-cwebp-readable@^2.0.1: version "2.0.1" @@ -5798,7 +5885,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -5860,13 +5947,6 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= -is-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" - is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -5908,22 +5988,17 @@ is-png@^1.0.0: resolved "https://registry.yarnpkg.com/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce" integrity sha1-1XSxK/J1wDUEVVcLDltXqwYgd84= -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== +is-regex@^1.0.4, is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== dependencies: - has "^1.0.3" + has-symbols "^1.0.1" is-regexp@^1.0.0: version "1.0.0" @@ -6023,9 +6098,9 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isbinaryfile@^4.0.2: - version "4.0.5" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9" - integrity sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA== + version "4.0.6" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" + integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== isexe@^2.0.0: version "2.0.0" @@ -6075,9 +6150,9 @@ jquery@>=1.12.0, jquery@^3.4.1, jquery@^3.5.1: integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== js-base64@^2.1.9: - version "2.5.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" - integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.1.tgz#c328374225d2e65569791ded73c258e2c59334c7" + integrity sha512-G5x2saUTupU9D/xBY9snJs3TxvwX8EkpLFiYlPpDt/VmMHOXprnSU1nxiTmFbijCX4BLF/cMRIfAcC5BiMYgFQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -6152,9 +6227,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e" - integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== dependencies: minimist "^1.2.5" @@ -6229,13 +6304,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -6276,67 +6344,39 @@ lines-and-columns@^1.1.6: integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= lint-staged@>=10: - version "10.0.9" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.0.9.tgz#185aabb2432e9467c84add306c990f1c20da3cdb" - integrity sha512-NKJHYgRa8oI9c4Ic42ZtF2XA6Ps7lFbXwg3q0ZEP0r55Tw3YWykCW1RzW6vu+QIGqbsy7DxndvKu93Wtr5vPQw== + version "10.2.11" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" + integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== dependencies: - chalk "^3.0.0" - commander "^4.0.1" + chalk "^4.0.0" + cli-truncate "2.1.0" + commander "^5.1.0" cosmiconfig "^6.0.0" debug "^4.1.1" dedent "^0.7.0" - execa "^3.4.0" - listr "^0.14.3" - log-symbols "^3.0.0" + enquirer "^2.3.5" + execa "^4.0.1" + listr2 "^2.1.0" + log-symbols "^4.0.0" micromatch "^4.0.2" normalize-path "^3.0.0" please-upgrade-node "^3.2.0" string-argv "0.3.1" stringify-object "^3.3.0" -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== +listr2@^2.1.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.1.8.tgz#8af7ebc70cdbe866ddbb6c80909142bd45758f1f" + integrity sha512-Op+hheiChfAphkJ5qUxZtHgyjlX9iNnAeFS/S134xw7mVSg0YVrQo1IY4/K+ElY6XgOPg2Ij4z07urUXR+YEew== dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== - dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" + chalk "^4.0.0" + cli-truncate "^2.1.0" + figures "^3.2.0" + indent-string "^4.0.0" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.5.5" + through "^2.3.8" load-grunt-tasks@^3.5.2: version "3.5.2" @@ -6382,15 +6422,6 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - loader-utils@^0.2.16: version "0.2.17" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" @@ -6470,13 +6501,6 @@ lodash@^3.10.0, lodash@^3.6.0, lodash@^4.0.0, lodash@^4.11.0, lodash@^4.17.10, l resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -6484,21 +6508,22 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== dependencies: - chalk "^2.4.2" + chalk "^4.0.0" -log-update@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: - ansi-escapes "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" log4js@~0.6.3: version "0.6.38" @@ -6615,13 +6640,6 @@ make-iterator@^1.0.0: dependencies: kind-of "^6.0.2" -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -6668,16 +6686,7 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@^0.4.1: +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -6720,9 +6729,9 @@ merge-stream@^2.0.0: integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.2.3, merge2@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" - integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" @@ -6764,17 +6773,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "1.43.0" + mime-db "1.44.0" mime@1.6.0: version "1.6.0" @@ -6782,9 +6791,9 @@ mime@1.6.0: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.0.3, mime@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== mime@~1.2: version "1.2.11" @@ -6796,7 +6805,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -6856,7 +6865,7 @@ minimatch@~0.2, minimatch@~0.2.11: lru-cache "2" sigmund "~1.0.0" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -6891,9 +6900,9 @@ mkdirp@0.3.x: integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: - version "0.5.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" - integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" @@ -6903,9 +6912,9 @@ mkdirp@~1.0.3: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.10.6, moment@^2.16.0, moment@^2.21.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== move-concurrently@^1.0.1: version "1.0.1" @@ -6982,9 +6991,9 @@ mute-stream@0.0.8: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.12.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== nan@~1.0.0: version "1.0.0" @@ -7114,12 +7123,10 @@ node-plop@~0.26.0: mkdirp "^0.5.1" resolve "^1.12.0" -node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" +node-releases@^1.1.53: + version "1.1.58" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" + integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== nopt@2.1.x: version "2.1.2" @@ -7160,7 +7167,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7248,14 +7255,17 @@ object-hash@^1.1.4: integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== object-is@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" - integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" + integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -7312,7 +7322,7 @@ object.pick@^1.2.0, object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0: +object.values@^1.1.0, object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== @@ -7366,9 +7376,9 @@ onetime@^5.1.0: mimic-fn "^2.1.0" opencollective-postinstall@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" - integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== opn@^5.5.0: version "5.5.0" @@ -7384,14 +7394,6 @@ optimist@0.3.5: dependencies: wordwrap "~0.0.2" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optimist@~0.3, optimist@~0.3.5: version "0.3.7" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" @@ -7461,15 +7463,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -7493,11 +7486,6 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-event@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-1.3.0.tgz#8e6b4f4f65c72bc5b6fe28b75eda874f96a4a085" @@ -7517,21 +7505,11 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -7540,9 +7518,9 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0, p-limit@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" - integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" @@ -7586,6 +7564,13 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-pipe@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" @@ -7655,7 +7640,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0: +parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== @@ -7783,7 +7768,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -7832,9 +7817,9 @@ pause@0.0.1: integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= pbkdf2@^3.0.3: - version "3.0.17" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" - integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" + integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -7847,7 +7832,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.5, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -7914,12 +7899,12 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= dependencies: - find-up "^3.0.0" + find-up "^2.1.0" please-upgrade-node@^3.2.0: version "3.2.0" @@ -8279,9 +8264,9 @@ postcss@^6.0.1, postcss@^6.0.17, postcss@^6.0.23: supports-color "^5.4.0" postcss@^7.0.0: - version "7.0.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -8308,9 +8293,9 @@ prepend-http@^2.0.0: integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= prettier@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.2.tgz#1ba8f3eb92231e769b7fcd7cb73ae1b6b74ade08" - integrity sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg== + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== pretty-error@^2.0.2: version "2.1.1" @@ -8472,7 +8457,7 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -8549,7 +8534,7 @@ read-pkg@^2.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -8577,6 +8562,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -8640,9 +8632,9 @@ regenerate-unicode-properties@^8.2.0: regenerate "^1.4.0" regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== + version "1.4.1" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" + integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== regenerator-runtime@^0.13.4: version "0.13.5" @@ -8691,9 +8683,9 @@ regexpu-core@^4.6.0, regexpu-core@^4.7.0: unicode-match-property-value-ecmascript "^1.2.0" regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== regjsparser@^0.6.4: version "0.6.4" @@ -8741,9 +8733,9 @@ repeating@^2.0.0: is-finite "^1.0.0" replace-ext@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== require-directory@^2.1.1: version "2.1.1" @@ -8825,24 +8817,17 @@ resolve@0.5.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.5.1.tgz#15e4a222c4236bcd4cf85454412c2d0fb6524576" integrity sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY= -resolve@^1.1.6, resolve@^1.1.7, resolve@~1.1.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" -resolve@^1.3.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== - dependencies: - path-parse "^1.0.5" +resolve@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= responselike@1.0.2: version "1.0.2" @@ -8934,11 +8919,9 @@ run-async@^0.1.0: once "^1.3.0" run-async@^2.2.0, run-async@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" - integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== - dependencies: - is-promise "^2.1.0" + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.1.9" @@ -8957,17 +8940,10 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^6.3.3, rxjs@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" - integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== - dependencies: - tslib "^1.9.0" - -rxjs@^6.4.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== +rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== dependencies: tslib "^1.9.0" @@ -8976,10 +8952,10 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-regex@^1.1.0: version "1.1.0" @@ -9016,11 +8992,12 @@ schema-utils@^1.0.0: ajv-keywords "^3.1.0" schema-utils@^2.6.5: - version "2.6.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.5.tgz#c758f0a7e624263073d396e29cd40aa101152d8a" - integrity sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ== + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: - ajv "^6.12.0" + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" ajv-keywords "^3.4.1" seek-bzip@^1.0.5: @@ -9106,10 +9083,12 @@ sentence-case@^2.1.0: no-case "^2.2.0" upper-case-first "^1.1.2" -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +serialize-javascript@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" + integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== + dependencies: + randombytes "^2.1.0" serve-index@^1.9.1: version "1.9.1" @@ -9216,9 +9195,9 @@ sigmund@~1.0.0: integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== simple-is@~0.2.0: version "0.2.0" @@ -9244,6 +9223,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snake-case@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" @@ -9361,9 +9358,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -9396,22 +9393,22 @@ source-map@~0.1.7: amdefine ">=0.0.4" spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" @@ -9451,6 +9448,11 @@ speed-measure-webpack-plugin@^1.2.3: dependencies: chalk "^2.0.1" +spinkit@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/spinkit/-/spinkit-2.0.1.tgz#aefcd0acfdf15a90aa8e1f069d7e618515891f74" + integrity sha512-oYBGY0GV1H1dX+ZdKnB6JVsYC1w/Xl20H111eb+WSS8nUYmlHgGb4y5buFSkzzceEeYYh5kMhXoAmoTpiQauiA== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -9557,7 +9559,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -9574,7 +9576,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -9583,30 +9585,23 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimleft@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" - integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== dependencies: define-properties "^1.1.3" - function-bind "^1.1.1" + es-abstract "^1.17.5" -string.prototype.trimright@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" - integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== dependencies: define-properties "^1.1.3" - function-bind "^1.1.1" + es-abstract "^1.17.5" -string_decoder@^1.0.0, string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -9618,6 +9613,13 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -9711,13 +9713,6 @@ style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" -supports-color@6.1.0, supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -9737,6 +9732,13 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -9784,11 +9786,6 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -9843,24 +9840,24 @@ tempfile@^2.0.0: uuid "^3.0.1" terser-webpack-plugin@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== + version "1.4.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f" + integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^2.1.2" + serialize-javascript "^3.1.0" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" terser@^4.1.2: - version "4.6.7" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72" - integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g== + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -9997,10 +9994,20 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@^1.9.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== tty-browserify@0.0.0: version "0.0.0" @@ -10063,12 +10070,9 @@ uglify-js@3.4.x: source-map "~0.6.1" uglify-js@^3.1.4: - version "3.8.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" - integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== - dependencies: - commander "~2.20.3" - source-map "~0.6.1" + version "3.10.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" + integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== uglify-js@~2.3: version "2.3.6" @@ -10085,9 +10089,9 @@ ui-select@^0.19.8: integrity sha1-dIYISKf9i8SU2YVtL2J3bqmGN8E= unbzip2-stream@^1.0.9: - version "1.3.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" - integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" through "^2.3.8" @@ -10322,10 +10326,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== v8flags@^2.0.10: version "2.1.1" @@ -10369,14 +10373,23 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -watchpack@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" - integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" + integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== dependencies: - chokidar "^2.0.2" graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.0" + watchpack-chokidar2 "^2.0.0" wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" @@ -10402,21 +10415,21 @@ webpack-build-notifier@^0.1.30: strip-ansi "^3.0.1" webpack-cli@^3.1.2: - version "3.3.11" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" - integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" webpack-dev-middleware@^3.7.2: version "3.7.2" @@ -10492,15 +10505,15 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-map "~0.6.1" webpack@^4.26.0: - version "4.42.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" - integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== + version "4.43.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" + integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.2.1" + acorn "^6.4.1" ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" @@ -10517,7 +10530,7 @@ webpack@^4.26.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.0" + watchpack "^1.6.1" webpack-sources "^1.4.1" websocket-driver@0.6.5: @@ -10528,18 +10541,18 @@ websocket-driver@0.6.5: websocket-extensions ">=0.1.1" websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: - http-parser-js ">=0.4.0 <0.4.11" + http-parser-js ">=0.5.1" safe-buffer ">=5.1.0" websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whet.extend@~0.9.9: version "0.9.9" @@ -10585,6 +10598,11 @@ wordwrap@0.0.x, wordwrap@~0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -10592,14 +10610,6 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -10609,6 +10619,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -10680,14 +10699,12 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^1.7.2: - version "1.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" - integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw== - dependencies: - "@babel/runtime" "^7.8.7" +yaml@^1.10.0, yaml@^1.7.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@^13.1.0, yargs-parser@^13.1.2: +yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -10695,23 +10712,6 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"