1
0
Fork 0
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:
zees-dev 2021-09-10 14:06:57 +12:00 committed by GitHub
parent e86a586651
commit 2a60b8fcdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 3055 additions and 139 deletions

View file

@ -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: '',
};
});
}
}

View file

@ -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>

View file

@ -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: '<',
},
});

View file

@ -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%;
}

View file

@ -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>

View file

@ -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',
},
});

View file

@ -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;
}
});
}
}

View file

@ -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>

View file

@ -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: '<',
},
});

View file

@ -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;
}
}

View file

@ -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 -->

View file

@ -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: '<',
},
});