feat: add --force flag support for helm upgrade (#505)

Add a "Force upgrade" checkbox in the upgrade modal footer that passes
the --force flag to helm upgrade, causing resources to be deleted and
recreated. Also fix the version selector flashing the URL input while
loading by showing a spinner.

Closes #505

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrei Pohilko
2026-03-17 16:18:00 +00:00
parent 4fb2eb099a
commit c5ae60a779
4 changed files with 28 additions and 8 deletions

View File

@@ -47,6 +47,7 @@ export const InstallReleaseChartModal = ({
const navigate = useNavigateWithSearchParams(); const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState<string>(""); const [userValues, setUserValues] = useState<string>("");
const [installError, setInstallError] = useState(""); const [installError, setInstallError] = useState("");
const [forceUpgrade, setForceUpgrade] = useState(false);
const { const {
namespace: queryNamespace, namespace: queryNamespace,
@@ -62,6 +63,7 @@ export const InstallReleaseChartModal = ({
error: versionsError, error: versionsError,
data: _versions = [], data: _versions = [],
isSuccess, isSuccess,
isLoading: isLoadingVersions,
} = useGetVersions(chartName); } = useGetVersions(chartName);
const [selectedVersionData, setSelectedVersionData] = useState<VersionData>(); const [selectedVersionData, setSelectedVersionData] = useState<VersionData>();
@@ -166,6 +168,9 @@ export const InstallReleaseChartModal = ({
} }
formData.append("version", selectedVersion || ""); formData.append("version", selectedVersion || "");
formData.append("values", userValues || releaseValues || ""); // if userValues is empty, we use the release values formData.append("values", userValues || releaseValues || ""); // if userValues is empty, we use the release values
if (forceUpgrade) {
formData.append("force", "true");
}
const url = `/api/helm/releases/${ const url = `/api/helm/releases/${
namespace ? namespace : "default" namespace ? namespace : "default"
}/${releaseName}`; }/${releaseName}`;
@@ -210,6 +215,18 @@ export const InstallReleaseChartModal = ({
/> />
} }
containerClassNames="w-full text-2xl h-2/3" containerClassNames="w-full text-2xl h-2/3"
bottomContent={
isUpgrade ? (
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={forceUpgrade}
onChange={(e) => setForceUpgrade(e.target.checked)}
/>
Force upgrade
</label>
) : undefined
}
actions={[ actions={[
{ {
id: "1", id: "1",
@@ -223,7 +240,9 @@ export const InstallReleaseChartModal = ({
}, },
]} ]}
> >
{!useURLMode && versions && isNoneEmptyArray(versions) ? ( {isLoadingVersions ? (
<Spinner />
) : !useURLMode && versions && isNoneEmptyArray(versions) ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<VersionToInstall <VersionToInstall
versions={versions} versions={versions}

View File

@@ -122,10 +122,9 @@ const Modal = ({
<div className="max-h-[calc(100vh_-_200px)] gap-6 overflow-y-auto p-4"> <div className="max-h-[calc(100vh_-_200px)] gap-6 overflow-y-auto p-4">
{children} {children}
</div> </div>
{bottomContent ? ( <div className="flex items-center justify-between rounded-b border-t border-gray-200 p-6">
<div className="p-5 text-sm">{bottomContent}</div> <div>{bottomContent}</div>
) : ( <div className="flex gap-2">
<div className="flex justify-end gap-2 rounded-b border-t border-gray-200 p-6">
{actions?.map((action) => ( {actions?.map((action) => (
<button <button
key={action.id} key={action.id}
@@ -145,7 +144,7 @@ const Modal = ({
</button> </button>
))} ))}
</div> </div>
)} </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -389,7 +389,8 @@ func (h *HelmHandler) Upgrade(c *gin.Context) {
} }
justTemplate := c.PostForm("preview") == "true" justTemplate := c.PostForm("preview") == "true"
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values) force := c.PostForm("force") == "true"
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, force, values)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return

View File

@@ -306,7 +306,7 @@ func (r *Release) GetRev(revNo int) (*Release, error) {
return nil, errorx.InternalError.New("No revision found for number %d", revNo) return nil, errorx.InternalError.New("No revision found for number %d", revNo)
} }
func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, values map[string]interface{}) (*release.Release, error) { func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, force bool, values map[string]interface{}) (*release.Release, error) {
r.mx.Lock() r.mx.Lock()
defer r.mx.Unlock() defer r.mx.Unlock()
@@ -340,6 +340,7 @@ func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, v
cmd.DryRunOption = "server" cmd.DryRunOption = "server"
} }
cmd.ResetValues = true cmd.ResetValues = true
cmd.Force = force
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings) chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
if err != nil { if err != nil {