Skip to content

Commit

Permalink
Add support for viewing and migrate Tiller releases (#330)
Browse files Browse the repository at this point in the history
* Make the release name configurable

* Use correct release name when upgrading

* Add Tiller releases to Applications view. Add migration form to generate Helm CRDs

* Rename helmCRDReleaseName and tillerReleaseName

* Catch error when chart doesn't exist

* Avoid using helm and tiller release names

* Minor review

* Bump helm-crd version

* Apply review

* Simplify code

* Verify app namespace
  • Loading branch information
andresmgot authored Jun 6, 2018
1 parent 232e8a0 commit b1c599a
Show file tree
Hide file tree
Showing 22 changed files with 588 additions and 67 deletions.
36 changes: 35 additions & 1 deletion dashboard/src/actions/apps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Dispatch } from "redux";
import { createAction, getReturnOfExpression } from "typesafe-actions";

import { App } from "../shared/App";
import Chart from "../shared/Chart";
import { HelmRelease } from "../shared/HelmRelease";
import { IApp, IChartVersion, IStoreState } from "../shared/types";
import { AppConflict, IApp, IChartVersion, IStoreState, MissingChart } from "../shared/types";

export const requestApps = createAction("REQUEST_APPS");
export const receiveApps = createAction("RECEIVE_APPS", (apps: IApp[]) => {
Expand Down Expand Up @@ -47,6 +49,7 @@ export function deleteApp(releaseName: string, namespace: string) {
return async (dispatch: Dispatch<IStoreState>): Promise<boolean> => {
try {
await HelmRelease.delete(releaseName, namespace);
await App.waitForDeletion(releaseName);
return true;
} catch (e) {
dispatch(errorDeleteApp(e));
Expand Down Expand Up @@ -82,6 +85,11 @@ export function deployChart(
if (resourceVersion) {
await HelmRelease.upgrade(releaseName, namespace, chartVersion, values);
} else {
const releaseExists = await App.exists(releaseName);
if (releaseExists) {
dispatch(errorApps(new AppConflict("Already exists")));
return false;
}
await HelmRelease.create(releaseName, namespace, chartVersion, values);
}
return true;
Expand All @@ -91,3 +99,29 @@ export function deployChart(
}
};
}

export function migrateApp(
chartVersion: IChartVersion,
releaseName: string,
namespace: string,
values?: string,
) {
return async (dispatch: Dispatch<IStoreState>): Promise<boolean> => {
try {
const chartExists = await Chart.exists(
chartVersion.relationships.chart.data.name,
chartVersion.attributes.version,
chartVersion.relationships.chart.data.repo.name,
);
if (!chartExists) {
dispatch(errorApps(new MissingChart("Not found")));
return false;
}
await HelmRelease.create(releaseName, namespace, chartVersion, values);
return true;
} catch (e) {
dispatch(errorApps(e));
return false;
}
};
}
71 changes: 71 additions & 0 deletions dashboard/src/components/AppMigrate/AppMigrate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from "react";

import { RouterAction } from "react-router-redux";
import { IApp, IChartState, IChartVersion } from "../../shared/types";
import { IAppRepository } from "../../shared/types";

import MigrateForm from "../../components/MigrateForm";

interface IAppMigrateProps {
app: IApp;
error: Error | undefined;
namespace: string;
releaseName: string;
repos: IAppRepository[];
selected: IChartState["selected"];
migrateApp: (
version: IChartVersion,
releaseName: string,
namespace: string,
values?: string,
) => Promise<boolean>;
getApp: (releaseName: string, namespace: string) => Promise<void>;
push: (location: string) => RouterAction;
fetchRepositories: () => Promise<void>;
}

class AppMigrate extends React.Component<IAppMigrateProps> {
public componentDidMount() {
const { fetchRepositories, releaseName, getApp, namespace } = this.props;
getApp(releaseName, namespace);
fetchRepositories();
}

public componentWillReceiveProps(nextProps: IAppMigrateProps) {
const { releaseName, getApp, namespace } = this.props;
if (nextProps.namespace !== namespace) {
getApp(releaseName, nextProps.namespace);
}
}

public render() {
const { app, repos } = this.props;
if (
!repos ||
!app ||
!app.data ||
!app.data.chart ||
!app.data.chart.metadata ||
!app.data.chart.metadata.version ||
!app.data.chart.values
) {
return <div>Loading</div>;
}
return (
<div>
<MigrateForm
{...this.props}
chartID={app.data.name}
chartVersion={app.data.chart.metadata.version}
chartValues={app.data.chart.values.raw}
chartName={app.data.chart.metadata.name || ""}
chartRepoName=""
chartRepoURL=""
chartRepoAuth=""
/>
</div>
);
}
}

export default AppMigrate;
3 changes: 3 additions & 0 deletions dashboard/src/components/AppMigrate/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AppMigrate from "./AppMigrate";

export default AppMigrate;
46 changes: 34 additions & 12 deletions dashboard/src/components/AppView/AppControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,52 @@ interface IAppControlsProps {
}

interface IAppControlsState {
migrate: boolean;
modalIsOpen: boolean;
redirectToAppList: boolean;
upgrade: boolean;
deleting: boolean;
}

class AppControls extends React.Component<IAppControlsProps, IAppControlsState> {
public state: IAppControlsState = {
deleting: false,
migrate: false,
modalIsOpen: false,
redirectToAppList: false,
upgrade: false,
};

public render() {
const { name, namespace } = this.props.app.data;
if (this.props.app.hr && this.props.app.hr.metadata) {
return (
<div className="AppControls">
<button className="button" onClick={this.handleUpgradeClick}>
Upgrade
</button>
{this.state.upgrade && <Redirect push={true} to={`/apps/ns/${namespace}/edit/${name}`} />}
<button className="button button-danger" onClick={this.openModel}>
Delete
</button>
<ConfirmDialog
onConfirm={this.handleDeleteClick}
modalIsOpen={this.state.modalIsOpen}
loading={this.state.deleting}
closeModal={this.closeModal}
/>
{this.state.redirectToAppList && <Redirect to={`/apps/ns/${namespace}`} />}
</div>
);
}
return (
<div className="AppControls">
<button className="button" onClick={this.handleUpgradeClick}>
Upgrade
</button>
{this.state.upgrade && <Redirect push={true} to={`/apps/ns/${namespace}/edit/${name}`} />}
<button className="button button-danger" onClick={this.openModel}>
Delete
<button className="button" onClick={this.handleMigrateClick}>
Migrate
</button>
<ConfirmDialog
onConfirm={this.handleDeleteClick}
modalIsOpen={this.state.modalIsOpen}
closeModal={this.closeModal}
/>
{this.state.redirectToAppList && <Redirect to={`/apps/ns/${namespace}`} />}
{this.state.migrate && (
<Redirect push={true} to={`/apps/ns/${namespace}/migrate/${name}`} />
)}
</div>
);
}
Expand All @@ -47,6 +64,10 @@ class AppControls extends React.Component<IAppControlsProps, IAppControlsState>
this.setState({ upgrade: true });
};

public handleMigrateClick = () => {
this.setState({ migrate: true });
};

public openModel = () => {
this.setState({
modalIsOpen: true,
Expand All @@ -60,6 +81,7 @@ class AppControls extends React.Component<IAppControlsProps, IAppControlsState>
};

public handleDeleteClick = async () => {
this.setState({ deleting: true });
const deleted = await this.props.deleteApp();
const s: Partial<IAppControlsState> = { modalIsOpen: false };
if (deleted) {
Expand Down
14 changes: 14 additions & 0 deletions dashboard/src/components/AppView/AppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
<main>
<div className="container">
{this.props.deleteError && this.renderError(this.props.deleteError, "delete")}
{!this.props.app.hr && this.renderMigrationNeeded()}
<div className="row collapse-b-tablet">
<div className="col-3">
<ChartInfo app={app} />
Expand Down Expand Up @@ -198,6 +199,19 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
);
}

private renderMigrationNeeded() {
return (
<div className="banner">
<div className="container container-small text-c">
<p className="margin-t-small">
This release is not being managed by Kubeapps. To be able to upgrade or delete this
release <strong>click on the "Migrate" button below</strong>.
</p>
</div>
</div>
);
}

private renderError(error: Error, action: string = "view") {
const { namespace, releaseName } = this.props;
switch (error.constructor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class AppRepoListItem extends React.Component<IAppRepoListItemProps, IApp
<ConfirmDialog
onConfirm={this.handleDeleteClick(repo.metadata.name)}
modalIsOpen={this.state.modalIsOpen}
loading={false}
closeModal={this.closeModal}
/>

Expand Down
31 changes: 18 additions & 13 deletions dashboard/src/components/ConfirmDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Modal from "react-modal";

interface IConfirmDialogProps {
modalIsOpen: boolean;
loading: boolean;
onConfirm: () => Promise<any>;
closeModal: () => Promise<any>;
}
Expand Down Expand Up @@ -39,19 +40,23 @@ class ConfirmDialog extends React.Component<IConfirmDialogProps, IConfirmDialogS
{this.state.error && (
<div className="padding-big margin-b-big bg-action">{this.state.error}</div>
)}
<div>Are you sure you want to delete this?</div>
<div>
<button className="button" onClick={this.props.closeModal}>
Cancel
</button>
<button
className="button button-primary button-danger"
type="submit"
onClick={this.props.onConfirm}
>
Delete
</button>
</div>
{this.props.loading === true ? (
<div> Loading ... </div>
) : (
<div>
<div> Are you sure you want to delete this? </div>
<button className="button" onClick={this.props.closeModal}>
Cancel
</button>
<button
className="button button-primary button-danger"
type="submit"
onClick={this.props.onConfirm}
>
Delete
</button>
</div>
)}
</Modal>
</div>
);
Expand Down
11 changes: 9 additions & 2 deletions dashboard/src/components/DeploymentForm/DeploymentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RouterAction } from "react-router-redux";

import { IServiceBinding } from "../../shared/ServiceBinding";
import {
AppConflict,
ForbiddenError,
IChartState,
IChartVersion,
Expand Down Expand Up @@ -89,7 +90,7 @@ class DeploymentForm extends React.Component<IDeploymentFormProps, IDeploymentFo
namespace = hr.metadata.namespace;
this.setState({
namespace,
releaseName: hr.metadata.name,
releaseName: hr.spec.releaseName,
});
} else {
this.setState({
Expand Down Expand Up @@ -291,7 +292,7 @@ class DeploymentForm extends React.Component<IDeploymentFormProps, IDeploymentFo
resourceVersion,
);
if (deployed) {
push(`/apps/ns/${namespace}/${namespace}-${releaseName}`);
push(`/apps/ns/${namespace}/${releaseName}`);
} else {
this.setState({ isDeploying: false });
}
Expand Down Expand Up @@ -326,6 +327,12 @@ class DeploymentForm extends React.Component<IDeploymentFormProps, IDeploymentFo
roles[0].verbs = ["create"];
}
switch (error && error.constructor) {
case AppConflict:
return (
<NotFoundErrorAlert
header={`The given release name already exists in the cluster. Choose a different one`}
/>
);
case ForbiddenError:
return (
<PermissionsErrorAlert
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/components/DeprovisionButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DeprovisionButton extends React.Component<IDeprovisionButtonProps, IDeprov
<ConfirmDialog
onConfirm={this.handleDeprovision}
modalIsOpen={this.state.modalIsOpen}
loading={false}
closeModal={this.closeModal}
/>

Expand Down
1 change: 1 addition & 0 deletions dashboard/src/components/FunctionView/FunctionControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class FunctionControls extends React.Component<IFunctionControlsProps, IFunction
</button>
<ConfirmDialog
onConfirm={this.handleDeleteClick}
loading={false}
modalIsOpen={this.state.modalIsOpen}
closeModal={this.closeModal}
/>
Expand Down
Loading

0 comments on commit b1c599a

Please sign in to comment.