mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(kubernetes): introduce kubernetes support (#3987)
* feat(kubernetes): fix duplicate published mode * feat(kubernetes): group port mappings by applications * feat(kubernetes): updated UX * feat(kubernetes): updated UX * feat(kubernetes): new applications list view * fix(kubernetes): applications - expand ports on row click * refactor(kubernetes): applications - replace old view with new * fix(kubernetes): disable access management for default resource pool * feat(kubernetes): app creation - limit stacks suggestion to selected resource pool * feat(kubernetes): do not allow access management on system resource pools * refactor(kubernetes): refactor services * create view node detail * compute node status * compute resource reservations * resource reservation progress bar * create applications node datatable * fix(kubernetes): fix invalid method name * feat(kubernetes): minor UI changes * feat(kubernetes): update application inspect UI * feat(kubernetes): add the ability to copy load balancer IP * fix(kubernetes): minor fixes on applications view * feat(kubernetes): set usage level info on progress bars * fix(kubernetes): fix an issue with duplicate pagination controls * fix(kubernetes): fix an issue with unexpandable items * refacto(kubernetes): clean status and resource computation * fix(kubernetes): remove a bad line * feat(kubernetes): update application detail view * feat(kubernetes): change few things on view * refacto(kubernetes): Corrections relative to PR #13 * refacto(kubernetes): remove old functions * feat(kubernetes): add application pod logs * fix(kubernetes): PR #13 * feat(kubernetes): Enable quotas by default * feat(kubernetes): allow non admin to have access to ressource pool list/detail view * feat(kubernetes): UI changes * fix(kubernetes): fix resource reservation computation in node view * fix(kubernetes): pods are correctly filter by app name * fix(kubernetes): nodeapplicationsdatatable is correctly reorder by cpu and memory * fix(kubernetes): nodeapplications datatable is correctly reorder on reload * feat(kubernetes): update podService * refacto(kubernetes): rename nodeInspect as node * refaceto(kubernetes): use colspan 6 instead of colspan 3 * refacto(kubernetes): use genericdatatablecontroller and make isadmin a binding * refacto(kubernetes): remove not needed lines * refacto(kubernetes) extract usageLevelInfo as html filter * refacto(kubernetes): no line break for params * refacto(kubernetes): change on node converter and filters * refacto(kubernetes): remove bad indentations * feat(kubernetes): add plain text informations about resources limits for non admibn user * refacto(kubernetes): ES6 format * refacto(kubernetes): format * refacto(kubernetes): format * refacto(kubernetes): add refresh callback for nodeapplicationsdatatable * refacto(kubernetes): change if else structure * refactor(kubernetes): files naming and format * fix(kubernetes): remove checkbox and actions on resourcespools view for non admin * feat(kubernetes): minor UI update * fix(kubernetes): bind this on getPodsApplications to allow it to access $async * fix(kubernetes): bind this on getEvents to allow it to access $async * fix(kubernetes): format * feat(kubernetes): minor UI update * feat(kubernetes): add support for container console * fix(kubernetes): fix a merge issue * feat(kubernetes): update container console UI * fix(api): fix typo * feat(api): proxy pod websocket to agent * fix(api): fix websocket pod proxy * refactor(kubernetes): uniformize k8s merge comments * refactor(kubernetes): update consoleController * feat(kubernetes): prevent the removal of the default resource pool (#38) * feat(kubernetes): show all applications running inside the resource pool (#35) * add new datatable * feat(kubernetes): add resource pool applications datatable to resource pool detail view * refacto(kubernetes): factorise computeResourceReservation * fix(kubernetes): colspan 6 to colspan 5 * fix(kubernetes): rename resourceReservationHelper into kubernetesResourceReservationHelper * fix(kubernetes): add await to avoid double diggest cycles * feat(kubernetes): add link to application name * fix(kubernetes): change kubernetes-resource-pool-applications-datatable table key * fix(kubernetes): change wording * feat(kubernetes): add proper support for persisted folders (#36) * feat(kubernetes): persistent volume mockups * feat(kubernetes): persistent volume mockups * feat(kubernetes): update persisted folders mockups * feat(kubernetes): endpoint configure storage access policies * fix(kubernetes): restrict advanced deployment to admin * refactor(kubernetes): storageclass service / rest / model * refactor(kubernetes): params/payload/converter pattern for deployments and daemonsets * feat(kubernetes): statefulset management for applications * fix(kubernets): associate application and pods * feat(kubernetes): statefulset support for applications * refactor(kubernetes): rebase on pportainer/k8s * fix(kubernetes): app create - invalid targetPort on loadbalancer * fix(kubernetes): internal services showed as loadbalancer * fix(kubernetes): service ports creation / parsing * fix(kubernetes): remove ports on headless services + ensure nodePort is used only for Cluster publishing * fix(kubernetes): delete headless service on statefulset delete * fix(kubernetes): statefulset replicas count display * refactor(kubernetes): rebase on pportainer/k8s * refactor(kubernetes): cleanup Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * fix(kubernetes): remove mockup routes * feat(kubernetes): only display applications running on node/in resource pool when there are any * feat(kubernetes): review resource reservations and leverage requests instead of limits (#40) * fix(kubernetes): filter resource reservation by app in node view (#48) * refactor(kubernetes): remove review comment * chore(version): bump version number * refactor(kubernetes): remove unused stacks view and components * feat(kubernetes): update CPU slider step to 0.1 for resource pools (#60) * feat(kubernetes): round up application CPU values (#61) * feat(kubernetes): add information about application resource reservat… (#62) * feat(kubernetes): add information about application resource reservations * feat(kubernetes): apply kubernetesApplicationCPUValue to application CPU reservation * refactor(kubernetes): services layer with models/converter/payloads (#64) * refactor(kubernetes): services layer with models/converter/payloads * refactor(kubernetes): file rename and comment update * style(kubernetes): replace strings double quotes with simple quotes Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * fix(kubernetes): filter application by node in node detail view (#69) * fix(kubernetes): filter applications by node * fix(kubernetes): remove js error * refactor(kubernetes): delete resource quota deletion process when deleting a resource pool (#68) * feat(kubernetes): enforce valid resource reservations and clarify its… (#70) * feat(kubernetes): enforce valid resource reservations and clarify its usage * feat(kubernetes): update instance count input behavior * feat(kubernetes): resource pools labels (#71) * feat(kubernetes): resource pools labels * fix(kubernetes): RP/RQ/LR owner label * feat(kubernetes): confirmation popup on RP delete (#76) * feat(kubernetes): application labels (#72) * feat(kubernetes): application labels * feat(kubernetes): display application owner in details when available * style(kubernetes): revert StackName column labels * fix(kubernetes): default displayed StackName * feat(kubernetes): remove RQ query across cluster (#73) * refactor(kubernetes): routes as components (#75) * refactor(kubernetes): routes as components * refactor(kubernetes): use component lifecycle hook * refactor(kubernetes): files naming consistency * fix(kubernetes): fix invalid component name for cluster view Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): update portaineruser cluster role policy rules (#78) * refactor(kubernetes): remove unused helper * fix(kubernetes): fix invalid reload link in cluster view * feat(kubernetes): add cluster resource reservation (#77) * feat(kubernetes): add cluster resource reservation * fix(kubernetes): filter resource reservation with applications * fix(kubernetes): fix indent * refacto(kubernetes): extract megabytes value calc as resourceReservationHelper method * fix(kubernetes): remove unused import * refacto(kubernetes): add resourcereservation model * fix(kubernetes): add parenthesis on arrow functions parameters * refacto(kubernetes): getpods in applicationService getAll * fix(kubernetes): let to const * fix(kubernetes): remove unused podservice * fix(kubernetes): fix computeResourceReservation * fix(kubernetes): app.pods to app.Pods everywhere and camelcase of this.ResourceReservation * feat(kubernetes): configurations list view (#74) * feat(kubernetes): add configuration list view * feat(kubernetes): add configurations datatable * feat(kubernetes): add item selection * feat(kubernetes): allow to remove configuration * feat(kubernetes): allow non admin user to see configurations * fix(kubernetes): configurations view as component * feat(kubernetes): remove stack property for secret and configurations * fix(kubernetes): update import * fix(kubernetes): remove secret delete payload * fix(kubernetes): rename configuration model * fix(kubernetes): remove configmap delete payload * fix(Kubernetes): fix configuration getAsync * fix(kubernetes): extract params as variables * refacto(kubernetes): extract configurations used lines as helper * fix(kubernetes): add verification of _.find return value * fix(kubernetes): fix kubernetes configurations datatable callback * refacto(Kubernetes): extract find before if * fix(kubernetes): replace this by KubernetesConfigurationHelper in static method * fix(Kubernetes): fix getASync Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * review(kubernetes): todo comments (#80) * feat(kubernetes): minor UI update * feat(kubernetes): round max cpu value in application creation * feat(kubernetes): minor UI update * fix(kubernetes): no-wrap resource reservation bar text (#89) * docs(kubernetes): add review for formValues to resource conversion (#91) * feat(kubernetes): configuration creation view (#82) * feat(kubernetes): create configuration view * feat(kubernetes): add advanced mode and create entry from file * fix(kubernetes): fix validation issues * fix(kubernetes): fix wording * fix(kubernetes): replace data by stringdata in secret payloads * fix(kubernetes): rename KubernetesConfigurationEntry to KubernetesConfigurationFormValuesDataEntry * refacto(kubernetes): add isSimple to formValues and change configuration creation pattern * fix(kubernetes): fix some bugs * refacto(kubernetes): renaming * fix(kubernetes): fix few bugs * fix(kubernetes): fix few bugs * review(kubernetes): refactor notices Co-authored-by: xAt0mZ <baron_l@epitech.eu> * feat(kubernetes): rename codeclimate file * feat(kubernetes): re-enable codeclimate * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(kubernetes): minor UI update * feat(project): update codeclimate * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(kubernetes): configuration details view (#93) * feat(kubernetes): configuration details view * fix(kubernetes): fix wording * fix(kubernetes): fix update button * fix(kubernetes): line indent * refacto(kubernetes): remove conversion * refacto(kubernetes): remove useless line * refacto(kubernetes): remove useless lines * fix(kubernetes): revert error handling * fix(kubernetes): fix wording * fix(kubernetes): revert line deletion * refacto(kubernetes): change data mapping * fix(kubernetes): create before delete * fix(kubernetes): fix duplicate bug * feat(kubernetes): configurations in application creation (#92) * feat(kubernetes): application configuration mockups * feat(kubernetes): update mockup * feat(kubernetes): app create - dynamic view for configurations * feat(kubernetes): app create - configuration support * refactor(kubernetes): more generic configuration conversion function Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): automatically display first entry in configuration creation * feat(kubernetes): minor UI update regarding applications and configurations * feat(kubernetes): update Cluster icon in sidebar * feat(kubernetes): volumes list view (#112) * feat(kubernetes): add a feedback panel on main views (#111) * feat(kubernetes): add a feedback panel on main views * feat(kubernetes): add feedback panel to volumes view * fix(kubernetes): isolated volumes showed as unused even when used (#116) * feat(kubernetes): remove limit range from Portainer (#119) * limits instead of requests (#121) * feat(kubernetes): volume details (#117) * feat(kubernetes): volume details * fix(kubernetes): yaml not showed * feat(kubernetes): expandable stacks list (#122) * feat(kubernetes): expandable stacks list * feat(kubernetes): minor UI update to stacks datatable Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): uibprogress font color (#129) * feat(kubernetes): minor UI update to resource reservation component * feat(kubernetes): automatically select a configuration * refactor(kubernetes): remove comment * feat(kubernetes): minor UI update * feat(kubernetes): add resource links and uniformize view headers (#133) * feat(kubernetes): prevent removal of system configurations (#128) * feat(kubernetes): prevent removal of system configurations * fix(kubernetes): KubernetesNamespaceHelper is not a function * refacto(kubernetes): change prevent removal pattern * fix(kubernetes): remove unused dependencies * fix(kubernetes): fix configuration used label (#123) * fix(kubernetes): fix used configurations * fix(kubernetes): remove console log * feat(kubernetes): rename configuration types (#127) * refacto(kubernetes): fix wording and use configMap instead of Basic in the code * feat(kubernetes): prevent the removal of system configuration * fix(kubernetes): remove feat on bad branch * fix(kubernetes): rename configuration types * refacto(kubernetes): use a numeric enum and add a filter to display the text type * refacto(kubernetes): fix wording and use configMap instead of Basic in the code * feat(kubernetes): prevent the removal of system configuration * fix(kubernetes): remove feat on bad branch * fix(kubernetes): rename configuration types * refacto(kubernetes): use a numeric enum and add a filter to display the text type * fix(kubernetes): rename file and not use default in switch case * feat(kubernetes): update advanced deployment UI/UX (#130) * feat(kubernetes): update advanced deployment UI/UX * feat(kubernetes): review HTML tags indentation * feat(kubernetes): applications stacks delete (#135) * fix(kubernetes): multinode resources reservations (#118) * fix(kubernetes): filter pods by node * fix(kubernetes): fix applications by node filter * fix(kubernetes): filter pods by node * Update app/kubernetes/views/cluster/node/nodeController.js Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> * feat(kubernetes): limit usage of pod console view (#136) * feat(kubernetes): add yaml and events to configuration details (#126) * feat(kubernetes): add yaml and events to configuration details * fix(kubernetes): fix errors on secret details view * fix(kubernetes): display only events related to configuration * fix(kubernetes): fix applications by node filter * fix(kubernetes): revert commit on bad branch * refacto(kubernetes): refacto configmap get yaml function * refacto(kubernetes): add yaml into converter * feat(kubernetes): improve application details (#141) * refactor(kubernetes): remove applications retrieval from volume service * feat(kubernetes): improve application details view * feat(kubernetes): update kompose binary version (#143) * feat(kubernetes): update kubectl version (#144) * refactor(kubernetes): rename portainer system namespace (#145) * feat(kubernetes): add a loading view indicator (#140) * feat(kubernetes): add an example of view loading indicator * refactor(css): remove comment * feat(kubernetes): updated loading pattern * feat(kubernetes): add loading indicator for resource pool views * feat(kubernetes): add loading indicator for deploy view * feat(kubernetes): add loading view indicator to dashboard * feat(kubernetes): add loading view indicator to configure view * feat(kubernetes): add loading indicator to configuration views * feat(kubernetes): add loading indicator to cluster views * feat(kubernetes): rebase on k8s branch * feat(kubernetes): update icon size * refactor(kubernetes): update indentation and tag format * feat(kubernetes): backend role validation for stack deployment (#147) * feat(kubernetes): show applications when volume is used * feat(kubernetes): set empty value when node is not set * feat(kubernetes): update configuration UI/UX * feat(kubernetes): update configuration UX * fix(kubernetes): Invalid value for a configuration (#139) * fix(kubernetes): Invalid value for a configuration * fix(kubernetes): remove auto JSON convertion for configMap ; apply it for RPool Accesses only * refactor(kubernetes): remove unneeded line * fix(kubernetes): remove default JSON parsing on configMap API retrieval Co-authored-by: xAt0mZ <baron_l@epitech.eu> * feat(kubernetes): applications table in configuration details (#154) * feat(kubernetes): Add the ability to filter system resources (#142) * feat(kubernetes): hide system configurations * feat(kubernetes): Add the ability to filter system resources * feat(kubernetes): add the ability to hide system resources on volumes * fix(kubernetes): fix few issue in volumesDatatableController * fix(kubernetes): fix applications / ports / stacks labels * feat(kubernetes): add volumes and configurations to dashboard (#152) * feat(kubernetes): event warning indicator (#150) * feat(kubernetes): event warning indicator for applications * refactor(kubernetes): refactor events indicator logic * feat(kubernetes): add event warning indicator to all resources * feat(kubernetes): fix missing YAML panel for node (#157) * feat(kubernetes): revised application details view (#159) * feat(kubernetes): revised application details view * refactor(kubernetes): remove comment * feat(kubernetes): rebase on k8s * refactor(kubernetes): remove extra line * feat(kubernetes): update kubernetes beta feedback panel locations (#161) * feat(kubernetes): stack logs (#160) * feat(kubernetes): stack logs * fix(kubernetes): ignore starting pods * fix(kubernetes): colspan on expandable stack applications table * feat(kubernetes): add an information message about system resources (#163) * fix(kubernetes): fix empty panel being display in cluster view (#165) * fix(kubernetes): Invalid CPU unit for node (#156) * fix(kubernetes): Invalid CPU unit for node * fix(kubernetes): Invalid CPU unit for node * refacto(kubernetes): extract parseCPU function in helper * refacto(kubernetes): rewrite parseCPU function * feat(kubernetes): add the kube-node-lease namespace to system namespaces (#177) * feat(kubernetes): tag system applications on node details view (#175) * feat(kubernetes): tag system applications on node details view * fix(kubernetes): remove system resources filter * feat(kubernetes): review UI/UX around volume size unit (#178) * feat(kubernetes): updates after review (#174) * feat(kubernetes): update access user message * feat(kubernetes): relocate resource pool to a specific form section * feat(kubernetes): review responsiveness of port mappings * feat(kubernetes): clarify table settings * feat(kubernetes): add resource reservation summary message * feat(kubernetes): review wording (#182) * feat(kubernetes): application stack edit (#179) * feat(kubernetes): update UI -- update action missing * feat(kubernetes): application stack update * feat(kubernetes): change services stacks * feat(kubernetes): hide default-tokens + prevent remove (#183) * feat(kubernetes): hide default-tokens + prevent remove * feat(kubernetes): do not display unused label for system configurations * fix(kubernetes): minor fix around showing system configurations Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): rebase on k8s branch (#180) * fix(kubernetes): prevent the display of system resources in dashboard (#186) * fix(kubernetes): prevent the display of system resources in dashboard * fix(kubernetes): prevent the display of frontend filtered resource pools * feat(kubernetes): support downward API for env vars in application details (#181) * feat(kubernetes): support downward API for env vars in application details * refactor(kubernetes): remove comment * feat(kubernetes): minor UI update * feat(kubernetes): remove Docker features (#189) * chore(version): bump version number (#187) * chore(version): bump version number * feat(kubernetes): disable update notice * feat(kubernetes): minor UI update * feat(kubernetes): minor UI update * feat(kubernetes): form validation (#170) * feat(kubernetes): add published node port value check * feat(kubernetes): add a dns compliant validation * fix(kubernetes): fix port range validation * feat(kubernetes): lot of form validation * feat(kubernetes): add lot of form validation * feat(kubernetes): persisted folders size validation * feat(kubernetes): persisted folder path should be unique * fix(kubernetes): fix createResourcePool button * fix(kubernetes): change few things * fix(kubernetes): fix slider memory * fix(kubernetes): fix duplicates on dynamic field list * fix(kubernetes): remove bad validation on keys * feat(kubernetes): minor UI enhancements and validation updates * feat(kubernetes): minor UI update * fix(kubernetes): revert on slider fix * review(kubernetes): add future changes to do * fix(kubernetes): add form validation on create application memory slider Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> Co-authored-by: xAt0mZ <baron_l@epitech.eu> * feat(kubernetes): remove Docker related content * feat(kubernetes): update build system to remove docker binary install * fix(kubernetes): fix an issue with missing user settings * feat(kubernetes): created column for apps and resource pools (#184) * feat(kubernetes): created column for apps and resource pools * feat(kubernetes): configurations and volumes owner * feat(kubernetes): rename datatables columns * fix(kubernetes): auto detect statefulset headless service name (#196) * fix(applications): display used configurations (#198) * feat(kubernetes): app details - display data access policy (#199) * feat(kubernetes): app details - display data access policy * feat(kubernetes): tooltip on data access info * feat(kubernetes): move DAP tooltip to end of line * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * fix(kubernetes): fix an issue when updating the local endpoint (#204) * fix(kubernetes): add unique key to configuration overriden key path field (#207) * feat(kubernetes): tag applications as external (#221) * feat(kubernetes): tag applications as external first approach * feat(kubernetes): tag applications as external * feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API (#222) * feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API * fix(kubernetes): only display b units in list and details views * feat(kubernetes): add note to application details (#212) * feat(kubernetes): add note to application details * fix(kubernetes): remove eslintcache * feat(kubernetes): update application note UI * feat(kubernetes): add an update button to the note form when a note is already associated to an app * feat(kubernetes): fix with UI changes * fix(kubernetes): change few things * fix(kubernetes): remove duplicate button * fix(kubernetes): just use a ternary Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): fix data persistence display for isolated DAP (#223) * feat(kubernetes): add a quick action to copy application name to clipboard (#225) * feat(kubernetes): revert useless converter changes (#228) * feat(kubernetes): edit application view (#200) * feat(kubernetes): application to formValues conversion * feat(kubernetes): extract applicationFormValues conversion as converter function * feat(kubernetes): draft app patch * feat(kubernetes): patch on all apps services + service service + pvc service * feat(kubernetes): move name to labels and use UUID as kubernetes Name + patch recreate if necessary * feat(kubernetes): move user app name to label and use UUID for Kubernetes Name field * feat(kubernetes): kubernetes service patch mechanism * feat(kubernetes): application edit * feat(kubernetes): remove stack edit on app details * feat(kubernetes): revert app name saving in label - now reuse kubernetes Name field * feat(kubernetes): remove the ability to edit the DAP * feat(kubernetes): cancel button on edit view * feat(kubernetes): remove ability to add/remove persisted folders for SFS edition * feat(kubernetes): minor UI update and action changes * feat(kubernetes): minor UI update * feat(kubernetes): remove ability to edit app volumes sizes + disable update button if no changes are made + codeclimate * fix(kubernetes): resource reservation sliders in app edit * fix(kubernetes): patch returned with 422 when trying to create nested objects * fix(kubernetes): changing app deployment type wasn't working (delete failure) * style(kubernetes): codeclimate * fix(kubernetes): app edit - limits sliders max value * feat(kubernetes): remove prefix on service name as we enforce DNS compliant app names * fix(kubernetes): edit app formvalues replica based on target replica count and not total pods count * fix(kubernetes): disable update for RWO on multi replica + delete service when changing app type * fix(kubernetes): app details running / target pods display * feat(kubernetes): add partial patch for app details view Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): disable edit capability for external and system apps (#233) * feat(kubernetes): minor UI update * fix(kubernetes): edit application issues (#235) * feat(kubernetes): disable edition of load balancer if it's in pending state * fix(kubernetes): now able to change from LB to other publishing types * feat(kuberntes): modal on edit click to inform on potential service interruption * feat(kubernetes): hide note when empty + add capability to collapse it * fix(kubernetes): UI/API desync + app update button enabled in some cases where it shouldn't be * fix(kubernetes): all apps are now using rolling updates with specific conditions * style(kubernetes): code indent * fix(kubernetes): disable sync process on endpoint init as current endpoint is not saved in client state * fix(kubernetes): sliders refresh on app create + app details bad display for sfs running pods * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): bump up kubectl version to v1.18.0 * feat(kubernetes): when refreshing a view, remember currently opened tabs (#226) * feat(kubernetes): When refreshing a view, remember currently opened tabs * fix(kubernetes): only persist the current tab inside the actual view * fix(kubernetes): not working with refresh in view header * fix(kubernetes): skip error on 404 headless service retrieval if missconfigured in sfs (#242) * refactor(kubernetes): use KubernetesResourcePoolService instead of KubernetesNamespaceService (#243) * fix(kubernetes): create service before app to enforce port availability (#239) * fix(kubernetes): external flag on application ports mappings datatable (#245) * refactor(kubernetes): remove unused KubernetesResourcePoolHelper (#246) * refactor(kubernetes): make all *service.getAllAsync functions consistent (#249) * feat(kubernetes): Tag external applications in the application table of the resource pool details view (#251) * feat(kubernetes): add ability to redeploy application (#240) * feat(kubernetes): add ability to redeploy application * feat(kubernetes): allow redeploy for external apps * Revert "feat(kubernetes): allow redeploy for external apps" This reverts commit 093375a7e93c1a07b845ebca1618da034a97fbcd. * refactor(kubernetes): use KubernetesPodService instead of REST KubernetesPods (#247) * feat(kubernetes): prevent configuration properties edition (#248) * feat(kubernetes): prevent configuration properties edition * feat(kubernetes): Relocate the Data/Actions to a separate panel * feat(kubernetes): remove unused functions * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * refactor(kubernetes): Simplify the FileReader usage (#254) * refactor(kubernetes): simplify FileReader usage * refactor(kubernetes): Simplify FileReader usage * refactor(kubernetes): rename e as event for readability * feat(kubernetes): Tag system Configs in the Config details view (#257) * refactor(kubernetes): Refactor the isFormValid function of multiple controllers (#253) * refactor(kubernetes): refactor isFormValid functions in configurations * refactor(kubernetes): refactor isformValid functions in create application * refactor(kubernetes): remove duplicate lines * refactor(kubernetes): remove commented line * feat(kubernetes): Tag external volumes and configs (#250) * feat(kubernetes): Tag external volumes and configs * feat(kubernetes): remove .eslintcache * feat(kubernetes): change few things * feat(kubernetes): don't tag system configuration as external * feat(kubernetes): minor UI update * feat(kubernetes): extract inline css and clean all tags Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * fix(kubernetes): daemon set edit (#258) * fix(kubernetes): persistent folder unit parsing * fix(kubernetes): edit daemonset on RWO storage * fix(kubernetes): external SFS had unlinked volumes (#264) * feat(kubernetes): prevent to override two different configs on the same filesystem path (#259) * feat(kubernetes): prevent to override two different configs on the same filesystem path * feat(kubernetes): The validation should only be triggered across Configurations. * feat(kubernetes): fix validations issues * feat(kubernetes): fix form validation * feat(kubernetes): fix few things * refactor(kubernetes): Review the code mirror component update for configurations (#260) * refactor(kubernetes): extract duplicate configuration code into a component * refactor(kubernetes): fix form validation issues * refactor(kubernetes): fix missing value * refactor(kubernetes): remove useless await * feat(kubernetes): Update the shared access policy configuration for Storage (#263) * feat(kubernetes): Update the shared access policy configuration for Storage * Update app/kubernetes/models/storage-class/models.js * feat(kubernetes): remove ROX references and checks Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> Co-authored-by: xAt0mZ <baron_l@epitech.eu> * feat(kubernetes): provide the remove/restore UX for environment variables when editing an application (#261) * feat(kubernetes): Provide the remove/restore UX for environment variables when editing an application * feat(kubernetes): fix ui issue * feat(kubernetes): change few things * fix(kubernetes): Invalid display for exposed ports in accessing the application section (#267) * feat(kubernetes): application rollback (#269) * feat(kubernetes): retrieve all versions of a deployment * feat(kubernetes): application history for all types * feat(kubernetes): deployment rollback * feat(kubernetes): daemonset / statefulset rollback * feat(kubernetes): remove the revision selector and rollback on previous version everytime * feat(kubernetes): minor UI changes Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): reservations should be computed based on requests instead of limits (#268) * feat(kubernetes): Reservations should be computed based on requests instead of limits * feat(kubernetes): use requests instead of limits in application details * feat(kubernetes): removes unused limits * feat(kubernetes): Not so useless * feat(kubernetes): use service selectors to bind apps and services (#270) * feat(kubernetes): use service selectors to bind apps and services * Update app/kubernetes/services/statefulSetService.js * style(kubernetes): remove comment block Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> * chore(version): bump version number * feat(kubernetes): update feedback panel text * chore(app): add prettier to k8s * style(app): apply prettier to k8s codebase * fix(kubernetes): Cannot read property 'port' of undefined (#272) * fix(kubernetes): Cannot read property 'port' of undefined * fix(kubernetes): concat app ports outside publishedports loop * fix(application): fix broken display of the persistence layer (#274) * chore(kubernetes): fix conflicts * chore(kubernetes): fix issues related to conflict resolution * refactor(kubernetes): refactor code related to conflict resolution * fix(kubernetes): fix a minor issue with assets import * chore(app): update yarn.lock * fix(application): ports mapping are now correctly detected (#300) * fix(build-system): fix missing docker binary download step * feat(kubernetes): application auto scaling details (#301) * feat(kubernetes): application auto scaling details * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com> * feat(kubernetes): Introduce a "used by" column in the volume list view (#303) Co-authored-by: xAt0mZ <baron_l@epitech.eu> Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com> Co-authored-by: xAt0mZ <xAt0mZ@users.noreply.github.com>
This commit is contained in:
parent
24528ecea8
commit
af6bea5acc
361 changed files with 20794 additions and 2349 deletions
|
@ -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
|
||||
}
|
||||
|
|
21
api/http/handler/auth/logout.go
Normal file
21
api/http/handler/auth/logout.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")}
|
||||
}
|
||||
|
|
73
api/http/handler/endpointproxy/proxy_kubernetes.go
Normal file
73
api/http/handler/endpointproxy/proxy_kubernetes.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/"):
|
||||
|
|
|
@ -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.
|
||||
|
|
58
api/http/handler/stacks/create_kubernetes_stack.go
Normal file
58
api/http/handler/stacks/create_kubernetes_stack.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
116
api/http/handler/websocket/pod.go
Normal file
116
api/http/handler/websocket/pod.go
Normal file
|
@ -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=<token>&endpointId=<endpointID>&namespace=<namespace>&podName=<podName>&containerName=<containerName>&command=<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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
109
api/http/proxy/factory/kubernetes.go
Normal file
109
api/http/proxy/factory/kubernetes.go
Normal file
|
@ -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
|
||||
}
|
79
api/http/proxy/factory/kubernetes/token.go
Normal file
79
api/http/proxy/factory/kubernetes/token.go
Normal file
|
@ -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
|
||||
}
|
69
api/http/proxy/factory/kubernetes/token_cache.go
Normal file
69
api/http/proxy/factory/kubernetes/token_cache.go
Normal file
|
@ -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)
|
||||
}
|
156
api/http/proxy/factory/kubernetes/transport.go
Normal file
156
api/http/proxy/factory/kubernetes/transport.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue