mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feat(helm/templates): helm app templates EE-943 (#5449)
* feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook <hookenz@gmail.com> * feat(helm): list and configure helm chart (#5431) * backport and tidyup code * --amend * using rocket icon for charts * helm chart bugfix - clear category button * added matomo analytics for helm chart install * fix web editor exit warning without changes * editor modified exit bugfix * fixed notifications typo * updated helm template text * helper text to convey slow helm templates load Co-authored-by: zees-dev <dev.786zshan@gmail.com> * removing redundant time-consuming api call by using prop attribute * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev <dev.786zshan@gmail.com> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * bugfix: kubectl shell not opening - bearer token bug * tidy go modules & remove yarn-error.log * removed redundant handler (not used) - to match EE * resolved merge conflicts, updated code * feat(helm/views): helm release and application views EE-1236 (#5529) * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook <hookenz@gmail.com> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * removed redundant handler (not used) - to match EE * feat(helm) display helm charts - backend EE-1236 * copy over components for new applications view EE-1236 * Add new applications datatable component * Add more migrated files * removed test not applicable to CE * baclkported EE app data table code to CE * removed redundant helm repo url * resolved conflicts, updated code * using endpoint middleware * PR review fixes * using constants, openapi updated Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: zees-dev <dev.786zshan@gmail.com> * fixed test conflicts, go linted * feat(helm/templates-add): helm templates add repo for user support EE-1278 (#5514) * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook <hookenz@gmail.com> * feat(helm) helm chart backport from ee EE-1311 (#5436) * Add missing defaultHelmRepoUrl and mock testing * Backport EE-1477 * Backport updates to helm tests from EE * add https by default changes and ssl to tls renaming from EE * Port install integration test. Disabled by default to pass CI checks * merged changes from EE for the integration test * kube proxy whitelist updated to support internal helm install command Co-authored-by: zees-dev <dev.786zshan@gmail.com> * Pull in all changes from tech review in EE-943 * feat(helm): add helm chart backport to ce EE-1409 (#5425) * EE-1311 Helm Chart Backport from EE * backport to ce Co-authored-by: Matt Hook <hookenz@gmail.com> * Pull in all changes from tech review in EE-943 * added helm to sidebar after rebase, sync CE with EE * backport EE-1278, squashed, diffed, updated * helm install openapi spec update * resolved conflicts, updated code * - matching ee codebase at 0afe57034449ee0e9f333d92c252a13995a93019 - helm install using endpoint middleware - remove trailing slash from added/persisted helm repo urls * feat(helm) use libhelm url validator and improved path assembly EE-1554 (#5561) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 (#5567) * feat(helm/userrepos) fix getting global repo for ordinary users EE-1562 * post review changes and further backported changes from EE * resolved conflicts, updated code * fixed helm_install handler unit test * user cannot add existing repo if suffix is '/' (#5571) * feat(helm/docs) fix broken swagger docs EE-1278 (#5572) * Fix swagger docs * minor correction * fix(helm): migrating code from user handler to helm handler (#5573) * - migrated user_helm_repos to helm endpoint handler - migrated api operations from user factory/service to helm factory/service - passing endpointId into helm service/factory as endpoint provider is deprecated * upgrade libhelm to hide secrets Co-authored-by: Matt Hook <hookenz@gmail.com> * removed duplicate file - due to merge conflict * dependency injection in helm factory Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook <hookenz@gmail.com> * kubernetes.templates -> kubernetes.templates.helm name conflict fix * Validate the URL added as a public helm repo (#5579) * fix(helm): helm app deletion fix EE-1581 (#5582) * updated helm lib to show correct error on uninstall failure * passing down helm app namespace on deletion * fix(k8s): EE-1591 non-admin users cannot deploy charts containing secrets (#5590) Co-authored-by: Simon Meng <simon.meng@portainer.io> * fix(helm): helm epic bugfixes EE-1582 EE-1593 (#5585) * - trim trailing slash and lowercase before persisting helm repo - browser helm templates url /kubernetes/templates/templates -> /kubernetes/templates/helm - fix publish url - fix helm repo add refresh - semi-fix k8s app expansion * Tidy up swagger documentation related to helm. Make json consistent * fixed helm release page for non-default namespaces * k8s app view table expansion bugfix * EE-1593: publish url load balancer fallback Co-authored-by: Matt Hook <hookenz@gmail.com> * k8s app list fix for charts with deployments containing multiple pods - which use the same label (#5599) * fix(kubernetes): app list view fix for secrets with long keys or values EE-1600 (#5600) * k8s app secrets key value text overflow ellipses * wrapping key value pairs instead of ellipses * fix(helm): helm apps bundling issue across different namespaces EE-1619 (#5602) * helm apps bundling issue across different namespaces * - code comments and indentation to ease reading - moved namespace calc out of loop * feat(helm/test) disable slow helm search test by default EE-1599 (#5598) * skip helm_repo_search as it's an integration test * switch to portainer built in integration test checker * make module order match EE * don't print test struct out when skipping integration test Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Co-authored-by: Matt Hook <hookenz@gmail.com> Co-authored-by: cong meng <mcpacino@gmail.com> Co-authored-by: Simon Meng <simon.meng@portainer.io>
This commit is contained in:
parent
e86a586651
commit
2a60b8fcdf
89 changed files with 3055 additions and 139 deletions
|
@ -0,0 +1,12 @@
|
|||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
|
||||
export default class {
|
||||
$onInit() {
|
||||
const secrets = (this.configurations || [])
|
||||
.filter((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET)
|
||||
.flatMap((config) => Object.entries(config.Data))
|
||||
.map(([key, value]) => ({ key, value }));
|
||||
|
||||
this.state = { secrets };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<div class="col-xs-12 form-section-title">
|
||||
Secrets
|
||||
</div>
|
||||
<table style="width: 50%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<sensitive-details ng-repeat="secret in $ctrl.state.secrets" key="{{ secret.key }}" value="{{ secret.value }}"> </sensitive-details>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,10 @@
|
|||
import angular from 'angular';
|
||||
import controller from './applications-datatable-details.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableDetails', {
|
||||
templateUrl: './applications-datatable-details.html',
|
||||
controller,
|
||||
bindings: {
|
||||
configurations: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
.published-url-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 3fr;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.publish-url-link {
|
||||
width: min-content;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div class="published-url-container">
|
||||
<div class="text-muted">
|
||||
Published URL
|
||||
</div>
|
||||
<a ng-href="{{ $ctrl.publishedUrl }}" target="_blank" class="publish-url-link"> <i class="fa fa-external-link-alt" aria-hidden="true"></i> {{ $ctrl.publishedUrl }} </a>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
import angular from 'angular';
|
||||
import './applications-datatable-url.css';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatableUrl', {
|
||||
templateUrl: './applications-datatable-url.html',
|
||||
bindings: {
|
||||
publishedUrl: '@',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
.secondary-heading {
|
||||
background-color: #e7f6ff !important;
|
||||
}
|
||||
|
||||
.secondary-body {
|
||||
background-color: #f1f9fd;
|
||||
}
|
||||
|
||||
.datatable-wide {
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.datatable-padding-vertical {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div ng-if="$ctrl.isPrimary" class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
|
@ -62,7 +62,7 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div ng-if="$ctrl.isPrimary" class="actionBar">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
|
@ -79,7 +79,7 @@
|
|||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<div ng-if="$ctrl.isPrimary" class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -94,13 +94,20 @@
|
|||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells" data-cy="k8sApp-appTable">
|
||||
<thead>
|
||||
<thead ng-class="{ 'secondary-heading': !$ctrl.isPrimary }">
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="k8sApp-selectAllCheckbox" />
|
||||
<label for="select_all"></label>
|
||||
<th class="datatable-wide">
|
||||
<span ng-if="$ctrl.isPrimary">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="k8sApp-selectAllCheckbox" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.expandAll()">
|
||||
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||
</a>
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
|
@ -152,22 +159,42 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||
ng-class="{ active: item.Checked }"
|
||||
ng-click="$ctrl.expandItem(item, !$ctrl.isItemExpanded(item))"
|
||||
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||
ng-class="{ active: item.Checked, interactive: $ctrl.isExpandable(item), 'secondary-body': !$ctrl.isPrimary }"
|
||||
pagination-id="$ctrl.tableKey"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.isSystemNamespace(item)" />
|
||||
<span ng-if="$ctrl.isPrimary" class="md-checkbox">
|
||||
<input
|
||||
id="select_{{ $index }}"
|
||||
type="checkbox"
|
||||
ng-model="item.Checked"
|
||||
ng-click="$ctrl.selectItem(item, $event); $event.stopPropagation()"
|
||||
ng-disabled="$ctrl.isSystemNamespace(item)"
|
||||
/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
|
||||
<a ng-if="$ctrl.isExpandable(item)">
|
||||
<i ng-class="{ 'fas fa-angle-down': $ctrl.isItemExpanded(item), 'fas fa-angle-right': !$ctrl.isItemExpanded(item) }" class="space-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="item.KubernetesApplications" ui-sref="kubernetes.helm({ name: item.Name, namespace: item.ResourcePool })" ng-click="$event.stopPropagation()"
|
||||
>{{ item.Name }}
|
||||
</a>
|
||||
<a
|
||||
ng-if="!item.KubernetesApplications"
|
||||
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })"
|
||||
ng-click="$event.stopPropagation()"
|
||||
>{{ item.Name }}
|
||||
</a>
|
||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||
</td>
|
||||
<td>{{ item.StackName || '-' }}</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation()">{{ item.ResourcePool }}</a>
|
||||
</td>
|
||||
<td title="{{ item.Image }}"
|
||||
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
||||
|
@ -176,7 +203,10 @@
|
|||
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">
|
||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
|
||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
|
||||
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code>
|
||||
<span ng-if="item.RunningPodsCount >= 0 && item.TotalPodsCount >= 0">
|
||||
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code>
|
||||
</span>
|
||||
<span ng-if="item.KubernetesApplications">{{ item.Status }}</span>
|
||||
</td>
|
||||
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.POD">
|
||||
{{ item.Pods[0].Status }}
|
||||
|
@ -184,7 +214,7 @@
|
|||
<td>
|
||||
<span ng-if="item.PublishedPorts.length">
|
||||
<span>
|
||||
<a ng-click="$ctrl.onPublishingModeClick(item)">
|
||||
<a ng-click="$ctrl.onPublishingModeClick(item); $event.stopPropagation()">
|
||||
<i class="fa {{ item.ServiceType | kubernetesApplicationServiceTypeIcon }}" aria-hidden="true" style="margin-right: 2px;"> </i>
|
||||
{{ item.ServiceType | kubernetesApplicationServiceTypeText }}
|
||||
</a>
|
||||
|
@ -194,6 +224,33 @@
|
|||
</td>
|
||||
<td>{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="$ctrl.isExpandable(item) && $ctrl.isItemExpanded(item)" ng-class="{ 'secondary-body': $ctrl.isPrimary && !item.KubernetesApplications }">
|
||||
<td></td>
|
||||
<td colspan="8" class="datatable-padding-vertical">
|
||||
<span ng-if="item.KubernetesApplications">
|
||||
<kubernetes-applications-datatable
|
||||
dataset="item.KubernetesApplications"
|
||||
table-key="{{ item.Id }}_table"
|
||||
order-by="Name"
|
||||
remove-action="$ctrl.removeAction"
|
||||
refresh-callback="$ctrl.refreshCallback"
|
||||
on-publishing-mode-click="($ctrl.onPublishingModeClick)"
|
||||
is-primary="false"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</span>
|
||||
<span ng-if="!item.KubernetesApplications">
|
||||
<kubernetes-applications-datatable-url
|
||||
ng-if="$ctrl.getPublishedUrl(item)"
|
||||
published-url="{{ $ctrl.getPublishedUrl(item) }}"
|
||||
></kubernetes-applications-datatable-url>
|
||||
<kubernetes-applications-datatable-details
|
||||
ng-if="$ctrl.hasConfigurationSecrets(item)"
|
||||
configurations="item.Configurations"
|
||||
></kubernetes-applications-datatable-details>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
|
@ -203,7 +260,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div ng-if="$ctrl.isPrimary" class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import './applicationsDatatable.css';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatable', {
|
||||
templateUrl: './applicationsDatatable.html',
|
||||
controller: 'KubernetesApplicationsDatatableController',
|
||||
|
@ -11,5 +13,6 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
|
|||
removeAction: '<',
|
||||
refreshCallback: '<',
|
||||
onPublishingModeClick: '<',
|
||||
isPrimary: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
|
||||
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
|
||||
'$scope',
|
||||
|
@ -10,12 +11,55 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
function ($scope, $controller, DatatableService, Authentication) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
var ctrl = this;
|
||||
const ctrl = this;
|
||||
|
||||
this.settings = Object.assign(this.settings, {
|
||||
showSystem: false,
|
||||
});
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
expandAll: false,
|
||||
expandedItems: [],
|
||||
});
|
||||
|
||||
this.expandAll = function () {
|
||||
this.state.expandAll = !this.state.expandAll;
|
||||
this.state.filteredDataSet.forEach((item) => this.expandItem(item, this.state.expandAll));
|
||||
};
|
||||
|
||||
this.isItemExpanded = function (item) {
|
||||
return this.state.expandedItems.includes(item.Id);
|
||||
};
|
||||
|
||||
this.isExpandable = function (item) {
|
||||
return item.KubernetesApplications || this.hasConfigurationSecrets(item) || !!this.getPublishedUrl(item).length;
|
||||
};
|
||||
|
||||
this.expandItem = function (item, expanded) {
|
||||
// collapse item
|
||||
if (!expanded) {
|
||||
this.state.expandedItems = this.state.expandedItems.filter((id) => id !== item.Id);
|
||||
// expanded item
|
||||
} else if (expanded && !this.state.expandedItems.includes(item.Id)) {
|
||||
this.state.expandedItems = [...this.state.expandedItems, item.Id];
|
||||
}
|
||||
DatatableService.setDataTableExpandedItems(this.tableKey, this.state.expandedItems);
|
||||
};
|
||||
|
||||
this.expandItems = function (storedExpandedItems) {
|
||||
this.state.expandedItems = storedExpandedItems;
|
||||
if (this.state.expandedItems.length === this.dataset.length) {
|
||||
this.state.expandAll = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.onDataRefresh = function () {
|
||||
const storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
|
||||
if (storedExpandedItems !== null) {
|
||||
this.expandItems(storedExpandedItems);
|
||||
}
|
||||
};
|
||||
|
||||
this.onSettingsShowSystemChange = function () {
|
||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||
};
|
||||
|
@ -25,6 +69,10 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
};
|
||||
|
||||
this.isSystemNamespace = function (item) {
|
||||
// if all charts in a helm app/release are in the system namespace
|
||||
if (item.KubernetesApplications && item.KubernetesApplications.length > 0) {
|
||||
return item.KubernetesApplications.some((app) => KubernetesNamespaceHelper.isSystemNamespace(app.ResourcePool));
|
||||
}
|
||||
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
|
||||
};
|
||||
|
||||
|
@ -32,6 +80,33 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem;
|
||||
};
|
||||
|
||||
this.getPublishedUrl = function (item) {
|
||||
// Map all ingress rules in published ports to their respective URLs
|
||||
const ingressUrls = item.PublishedPorts.flatMap((pp) => pp.IngressRules)
|
||||
.filter(({ Host, IP }) => Host || IP)
|
||||
.map(({ Host, IP, Port, Path }) => {
|
||||
let scheme = Port === 443 ? 'https' : 'http';
|
||||
let urlPort = Port === 80 || Port === 443 ? '' : `:${Port}`;
|
||||
return `${scheme}://${Host || IP}${urlPort}${Path}`;
|
||||
});
|
||||
|
||||
// Map all load balancer service ports to ip address
|
||||
let loadBalancerURLs = [];
|
||||
if (item.LoadBalancerIPAddress) {
|
||||
loadBalancerURLs = item.PublishedPorts.map((pp) => `http://${item.LoadBalancerIPAddress}:${pp.Port}`);
|
||||
}
|
||||
|
||||
// combine ingress urls
|
||||
const publishedUrls = [...ingressUrls, ...loadBalancerURLs];
|
||||
|
||||
// Return the first URL - priority given to ingress urls, then services (load balancers)
|
||||
return publishedUrls.length > 0 ? publishedUrls[0] : '';
|
||||
};
|
||||
|
||||
this.hasConfigurationSecrets = function (item) {
|
||||
return item.Configurations && item.Configurations.some((config) => config.Data && config.Type === KubernetesConfigurationTypes.SECRET);
|
||||
};
|
||||
|
||||
/**
|
||||
* Do not allow applications in system namespaces to be selected
|
||||
*/
|
||||
|
@ -47,19 +122,19 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
const storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
|
||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
const textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
this.state.textFilter = textFilter;
|
||||
this.onTextFilterChange();
|
||||
}
|
||||
|
||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
const storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
if (storedFilters !== null) {
|
||||
this.filters = storedFilters;
|
||||
}
|
||||
|
@ -67,7 +142,12 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
this.filters.state.open = false;
|
||||
}
|
||||
|
||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||
const storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
|
||||
if (storedExpandedItems !== null) {
|
||||
this.expandItems(storedExpandedItems);
|
||||
}
|
||||
|
||||
const storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
this.settings.open = false;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
export default class HelmAddRepositoryController {
|
||||
/* @ngInject */
|
||||
constructor($state, $async, HelmService, Notifications, EndpointProvider) {
|
||||
this.$state = $state;
|
||||
this.$async = $async;
|
||||
this.HelmService = HelmService;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
}
|
||||
|
||||
doesRepoExist() {
|
||||
if (!this.state.repository) {
|
||||
return false;
|
||||
}
|
||||
// lowercase, strip trailing slash and compare
|
||||
return this.repos.includes(this.state.repository.toLowerCase().replace(/\/$/, ''));
|
||||
}
|
||||
|
||||
async addRepository() {
|
||||
this.state.isAddingRepo = true;
|
||||
try {
|
||||
await this.HelmService.addHelmRepository(this.EndpointProvider.currentEndpoint().Id, { url: this.state.repository });
|
||||
this.Notifications.success('Helm repository added successfully');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Installation error', err);
|
||||
} finally {
|
||||
this.state.isAddingRepo = false;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
isAddingRepo: false,
|
||||
repository: '',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<rd-widget>
|
||||
<rd-widget-header icon="fa-dharmachakra" title-text="Additional repositories"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="actionBar">
|
||||
<form class="form-horizontal" name="addUserHelmRepoForm">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Add a Helm repository. All Helm charts in the repository will be added to the list.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<input
|
||||
type="url"
|
||||
name="repo"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.repository"
|
||||
placeholder="https://charts.bitnami.com/bitnami"
|
||||
ng-pattern="/^https?:///"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nomargin" ng-show="addUserHelmRepoForm.repo.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="addUserHelmRepoForm.repo.$error">
|
||||
<p ng-message="pattern"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A valid URL beginning with http(s) is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="$ctrl.doesRepoExist()">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Helm repo already exists. </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-default nomargin"
|
||||
ng-click="$ctrl.addRepository()"
|
||||
ng-disabled="$ctrl.state.isAddingRepo || addUserHelmRepoForm.repo.$invalid || $ctrl.doesRepoExist()"
|
||||
analytics-on
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-helm-add-repository"
|
||||
>
|
||||
Add repository
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -0,0 +1,11 @@
|
|||
import angular from 'angular';
|
||||
import controller from './helm-add-repository.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmAddRepository', {
|
||||
templateUrl: './helm-add-repository.html',
|
||||
controller,
|
||||
bindings: {
|
||||
repos: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
.helm-template-item-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.helm-template-item-details .helm-template-item-details-sub {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<!-- helm chart -->
|
||||
<div ng-class="{ 'blocklist-item--selected': $ctrl.model.Selected }" class="blocklist-item template-item" ng-click="$ctrl.onSelect($ctrl.model)">
|
||||
<div class="blocklist-item-box">
|
||||
<!-- helmchart-image -->
|
||||
<span ng-if="$ctrl.model.icon">
|
||||
<img class="blocklist-item-logo" ng-src="{{ $ctrl.model.icon }}" />
|
||||
</span>
|
||||
<span class="blocklist-item-logo" ng-if="!$ctrl.model.icon">
|
||||
<i class="fa fa-dharmachakra fa-4x blue-icon" aria-hidden="true"></i>
|
||||
</span>
|
||||
<!-- !helmchart-image -->
|
||||
<!-- helmchart-details -->
|
||||
<div class="col-sm-12 helm-template-item-details">
|
||||
<!-- blocklist-item-line1 -->
|
||||
<div class="blocklist-item-line">
|
||||
<span>
|
||||
<span class="blocklist-item-title">
|
||||
{{ $ctrl.model.name }}
|
||||
</span>
|
||||
<span class="space-left blocklist-item-subtitle">
|
||||
<span>
|
||||
<i class="fa fa-dharmachakra" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>
|
||||
Helm
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line1 -->
|
||||
<span class="blocklist-item-actions" ng-transclude="actions"></span>
|
||||
<!-- blocklist-item-line2 -->
|
||||
<div class="blocklist-item-line helm-template-item-details-sub">
|
||||
<span class="blocklist-item-desc">
|
||||
{{ $ctrl.model.description }}
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="$ctrl.model.annotations.category">
|
||||
{{ $ctrl.model.annotations.category }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- !blocklist-item-line2 -->
|
||||
</div>
|
||||
<!-- !helmchart-details -->
|
||||
</div>
|
||||
<!-- !helm chart -->
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
import angular from 'angular';
|
||||
import './helm-templates-list-item.css';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesListItem', {
|
||||
templateUrl: './helm-templates-list-item.html',
|
||||
bindings: {
|
||||
model: '<',
|
||||
onSelect: '<',
|
||||
},
|
||||
transclude: {
|
||||
actions: '?templateItemActions',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
export default class HelmTemplatesListController {
|
||||
/* @ngInject */
|
||||
constructor($async, DatatableService, HelmService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.DatatableService = DatatableService;
|
||||
this.HelmService = HelmService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.updateCategories = this.updateCategories.bind(this);
|
||||
}
|
||||
|
||||
async updateCategories() {
|
||||
try {
|
||||
const annotationCategories = this.charts
|
||||
.map((t) => t.annotations) // get annotations
|
||||
.filter((a) => a) // filter out undefined/nulls
|
||||
.map((c) => c.category); // get annotation category
|
||||
const availableCategories = [...new Set(annotationCategories)].sort(); // unique and sort
|
||||
this.state.categories = availableCategories;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm charts categories');
|
||||
}
|
||||
}
|
||||
|
||||
onTextFilterChange() {
|
||||
this.DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||
}
|
||||
|
||||
clearCategory() {
|
||||
this.state.selectedCategory = '';
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
if (this.charts.length > 0) {
|
||||
this.updateCategories();
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
textFilter: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
const textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
this.state.textFilter = textFilter;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
|
||||
<div class="actionBar">
|
||||
<div>
|
||||
<span style="width: 25%;">
|
||||
<ui-select ng-model="$ctrl.state.selectedCategory" theme="bootstrap">
|
||||
<ui-select-match placeholder="Select a category">
|
||||
<a class="btn btn-xs btn-link pull-right" ng-click="$ctrl.clearCategory()"><i class="glyphicon glyphicon-remove"></i></a>
|
||||
<span>{{ $select.selected }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="category in ($ctrl.state.categories | filter: $select.search)">
|
||||
<span>{{ category }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="blocklist">
|
||||
<helm-templates-list-item
|
||||
ng-repeat="chart in $ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory "
|
||||
model="chart"
|
||||
type-label="helm"
|
||||
on-select="($ctrl.selectAction)"
|
||||
>
|
||||
</helm-templates-list-item>
|
||||
<div ng-if="$ctrl.loading" class="text-center text-muted">
|
||||
Loading...
|
||||
<div class="text-center text-muted">
|
||||
Initial download of Helm Charts can take a few minutes
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.loading && $ctrl.charts.length === 0" class="text-center text-muted">
|
||||
No helm charts available.
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
import angular from 'angular';
|
||||
import controller from './helm-templates-list.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesList', {
|
||||
templateUrl: './helm-templates-list.html',
|
||||
controller,
|
||||
bindings: {
|
||||
loading: '<',
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
charts: '<',
|
||||
tableKey: '@',
|
||||
selectAction: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,183 @@
|
|||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
|
||||
export default class HelmTemplatesController {
|
||||
/* @ngInject */
|
||||
constructor($analytics, $async, $state, $window, $anchorScroll, Authentication, HelmService, KubernetesResourcePoolService, Notifications, ModalService) {
|
||||
this.$analytics = $analytics;
|
||||
this.$async = $async;
|
||||
this.$window = $window;
|
||||
this.$state = $state;
|
||||
this.$anchorScroll = $anchorScroll;
|
||||
this.Authentication = Authentication;
|
||||
this.HelmService = HelmService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.Notifications = Notifications;
|
||||
this.ModalService = ModalService;
|
||||
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.uiCanExit = this.uiCanExit.bind(this);
|
||||
this.installHelmchart = this.installHelmchart.bind(this);
|
||||
this.getHelmValues = this.getHelmValues.bind(this);
|
||||
this.selectHelmChart = this.selectHelmChart.bind(this);
|
||||
this.getHelmRepoURLs = this.getHelmRepoURLs.bind(this);
|
||||
this.getLatestCharts = this.getLatestCharts.bind(this);
|
||||
this.getResourcePools = this.getResourcePools.bind(this);
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if (this.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
editorUpdate(content) {
|
||||
const contentvalues = content.getValue();
|
||||
if (this.state.originalvalues === contentvalues) {
|
||||
this.state.isEditorDirty = false;
|
||||
} else {
|
||||
this.state.values = contentvalues;
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
if (this.state.isEditorDirty) {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
async installHelmchart() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const payload = {
|
||||
Name: this.state.appName,
|
||||
Repo: this.state.chart.repo,
|
||||
Chart: this.state.chart.name,
|
||||
Values: this.state.values,
|
||||
Namespace: this.state.resourcePool.Namespace.Name,
|
||||
};
|
||||
await this.HelmService.install(this.endpoint.Id, payload);
|
||||
this.Notifications.success('Helm Chart successfully installed');
|
||||
this.$analytics.eventTrack('kubernetes-helm-install', { category: 'kubernetes', metadata: { 'chart-name': this.state.chart.name } });
|
||||
this.state.isEditorDirty = false;
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Installation error', err);
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getHelmValues() {
|
||||
this.state.loadingValues = true;
|
||||
try {
|
||||
const { values } = await this.HelmService.values(this.state.chart.repo, this.state.chart.name);
|
||||
this.state.values = values;
|
||||
this.state.originalvalues = values;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm chart values.');
|
||||
} finally {
|
||||
this.state.loadingValues = false;
|
||||
}
|
||||
}
|
||||
|
||||
async selectHelmChart(chart) {
|
||||
this.$anchorScroll('view-top');
|
||||
this.state.showCustomValues = false;
|
||||
this.state.chart = chart;
|
||||
await this.getHelmValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to get the helm repo urls for the endpoint and user
|
||||
* @returns {Promise<string[]>} list of helm repo urls
|
||||
*/
|
||||
async getHelmRepoURLs() {
|
||||
this.state.reposLoading = true;
|
||||
try {
|
||||
// fetch globally set helm repo and user helm repos (parallel)
|
||||
const { GlobalRepository, UserRepositories } = await this.HelmService.getHelmRepositories(this.endpoint.Id);
|
||||
const userHelmReposUrls = UserRepositories.map((repo) => repo.URL);
|
||||
const uniqueHelmRepos = [...new Set([GlobalRepository, ...userHelmReposUrls])].map((url) => url.toLowerCase()); // remove duplicates, to lowercase
|
||||
this.state.repos = uniqueHelmRepos;
|
||||
return uniqueHelmRepos;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo urls.');
|
||||
} finally {
|
||||
this.state.reposLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to fetch the respective index.yaml files for the provided helm repo urls
|
||||
* @param {string[]} helmRepos list of helm repositories
|
||||
* @param {bool} append append charts returned from repo to existing list of helm charts
|
||||
*/
|
||||
async getLatestCharts(helmRepos) {
|
||||
this.state.chartsLoading = true;
|
||||
try {
|
||||
const promiseList = helmRepos.map((repo) => this.HelmService.search(repo));
|
||||
// fetch helm charts from all the provided helm repositories (parallel)
|
||||
// Promise.allSettled is used to account for promise failure(s) - in cases the user has provided invalid helm repo
|
||||
const chartPromises = await Promise.allSettled(promiseList);
|
||||
const latestCharts = chartPromises
|
||||
.filter((tp) => tp.status === 'fulfilled') // remove failed promises
|
||||
.map((tp) => ({ entries: tp.value.entries, repo: helmRepos[chartPromises.indexOf(tp)] })) // extract chart entries with respective repo data
|
||||
.flatMap(
|
||||
({ entries, repo }) => Object.values(entries).map((charts) => ({ ...charts[0], repo })) // flatten chart entries to single array with respective repo
|
||||
);
|
||||
|
||||
this.state.charts = latestCharts;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm repo charts.');
|
||||
} finally {
|
||||
this.state.chartsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getResourcePools() {
|
||||
this.state.resourcePoolsLoading = true;
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
this.state.resourcePools = nonSystemNamespaces;
|
||||
this.state.resourcePool = nonSystemNamespaces[0];
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve initial helm data.');
|
||||
} finally {
|
||||
this.state.resourcePoolsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
appName: '',
|
||||
chart: null,
|
||||
showCustomValues: false,
|
||||
actionInProgress: false,
|
||||
resourcePools: [],
|
||||
resourcePool: '',
|
||||
values: null,
|
||||
originalvalues: null,
|
||||
repos: [],
|
||||
charts: [],
|
||||
loadingValues: false,
|
||||
isEditorDirty: false,
|
||||
chartsLoading: false,
|
||||
resourcePoolsLoading: false,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
const helmRepos = await this.getHelmRepoURLs();
|
||||
await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]);
|
||||
|
||||
this.state.viewReady = true;
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.state.isEditorDirty = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
<rd-header id="view-top">
|
||||
<rd-header-title title-text="Helm">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="kubernetes.templates.helm" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Charts</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<information-panel title-text="Information" ng-if="!$ctrl.state.chart">
|
||||
<span class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This is a first version for Helm charts, for more information see this <a href="#">blog post.</a>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row">
|
||||
<!-- helmchart-form -->
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.chart">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="$ctrl.state.chart.icon" title-text="$ctrl.state.chart.name"></rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal" name="$ctrl.helmTemplateCreationForm">
|
||||
<!-- description -->
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Description
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="template-note" ng-bind-html="$ctrl.state.chart.description"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !description -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Configuration
|
||||
</div>
|
||||
<!-- namespace-input -->
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-2 control-label text-left">Namespace</label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
class="form-control"
|
||||
id="resource-pool-selector"
|
||||
ng-model="$ctrl.state.resourcePool"
|
||||
ng-options="resourcePool.Namespace.Name for resourcePool in $ctrl.state.resourcePools"
|
||||
ng-change=""
|
||||
ng-disabled="$ctrl.state.isEdit"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!$ctrl.state.resourcePool">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
|
||||
namespace.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!$ctrl.state.resourcePool">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You do not have access to any namespace. Contact your administrator to get access to a namespace.
|
||||
</div>
|
||||
</div>
|
||||
<!-- !namespace-input -->
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="release_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="text"
|
||||
name="release_name"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.appName"
|
||||
placeholder="e.g. my-app"
|
||||
required
|
||||
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="$ctrl.helmTemplateCreationForm.release_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="$ctrl.helmTemplateCreationForm.release_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters or '-', start with an alphabetic
|
||||
character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive" ng-if="!$ctrl.state.showCustomValues && !$ctrl.state.loadingValues" ng-click="$ctrl.state.showCustomValues = true;">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show custom values
|
||||
</a>
|
||||
<span class="small interactive" ng-if="$ctrl.state.loadingValues"> <i class="fa fa-sync-alt space-right" aria-hidden="true"></i> Loading values.yaml... </span>
|
||||
<a class="small interactive" ng-if="$ctrl.state.showCustomValues" ng-click="$ctrl.state.showCustomValues = false;">
|
||||
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide custom values
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- values override -->
|
||||
<div ng-if="$ctrl.state.showCustomValues">
|
||||
<!-- web-editor -->
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can get more information about Helm values file format in the
|
||||
<a href="https://helm.sh/docs/chart_template_guide/values_files/" target="_blank">official documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="helm-app-creation-editor"
|
||||
placeholder="# Define or paste the content of your values yaml file here"
|
||||
yml="true"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
value="$ctrl.state.values"
|
||||
></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
</div>
|
||||
<!-- !values override -->
|
||||
<!-- helm actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!($ctrl.state.appName && $ctrl.state.resourcePool && !$ctrl.state.loadingValues && !$ctrl.state.actionInProgress)"
|
||||
ng-click="$ctrl.installHelmchart()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Install</span>
|
||||
<span ng-hide="!$ctrl.state.actionInProgress">Helm installing in progress</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="$ctrl.state.chart = null">Hide</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !helm actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<!-- helmchart-form -->
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<helm-add-repository repos="$ctrl.state.repos"></helm-add-repository>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helm Charts Component -->
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<helm-templates-list
|
||||
title-text="Charts"
|
||||
title-icon="fa-rocket"
|
||||
charts="$ctrl.state.charts"
|
||||
table-key="$ctrl.state.charts"
|
||||
select-action="$ctrl.selectHelmChart"
|
||||
loading="$ctrl.state.chartsLoading || $ctrl.state.resourcePoolsLoading"
|
||||
>
|
||||
</helm-templates-list>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Helm Charts Component -->
|
|
@ -0,0 +1,10 @@
|
|||
import angular from 'angular';
|
||||
import controller from './helm-templates.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('helmTemplatesView', {
|
||||
templateUrl: './helm-templates.html',
|
||||
controller,
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
|
@ -28,6 +28,10 @@
|
|||
Namespaces
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item path="kubernetes.templates.helm" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-dharmachakra fa-fw" class-name="sidebar-list">
|
||||
Helm
|
||||
</sidebar-menu-item>
|
||||
|
||||
<sidebar-menu-item
|
||||
path="kubernetes.applications"
|
||||
path-params="{ endpointId: $ctrl.endpointId }"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue