Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Locking with optional deployment #431

Draft
wants to merge 3 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<div class="flex items-center justify-between mb-3">
<input pInputText id="commit-hash" (input)="onSearch($event)" [value]="searchInput()" type="text" placeholder="Search for installed systems" class="w-1/3" />
<input pInputText id="commit-hash" (input)="onSearch($event)" [value]="searchInput()" type="text"
placeholder="Search for installed systems" class="w-1/3"/>
@if (!hideLinkToList()) {
<p-button [routerLink]="'../../../environment'" class="self-end">{{ isAdmin() ? 'Manage Environments' : 'Go to Environments' }}</p-button>
<p-button [routerLink]="'../../../environment'"
class="self-end">{{ isAdmin() ? 'Manage Environments' : 'Go to Environments' }}
</p-button>
}
</div>

Expand All @@ -20,31 +23,34 @@
<div class="flex gap-2 items-center w-full">
<div class="flex flex-col gap-1">
<div class="flex gap-1 items-center mr-3">
<span [pTooltip]="'Open Environment'" class="cursor-pointer hover:bg-gray-200 px-2 py-1 rounded" (click)="openExternalLink($event, environment)">
<span [pTooltip]="'Open Environment'" class="cursor-pointer hover:bg-gray-200 px-2 py-1 rounded"
(click)="openExternalLink($event, environment)">
{{ environment.name }}
</span>

<app-lock-tag [isLocked]="!!environment.locked"></app-lock-tag>

@if (environment.latestStatus; as status) {
<app-environment-status-tag [status]="status" />
<app-environment-status-tag [status]="status"/>
}

@if (environment.type) {
<p-tag [value]="formatEnvironmentType(environment.type)" severity="secondary" rounded="true" />
<p-tag [value]="formatEnvironmentType(environment.type)" severity="secondary" rounded="true"/>
}
</div>
@if (environment.latestDeployment; as deployment) {
<div class="flex gap-1 items-center text-sm mt-2">
<app-user-avatar [user]="environment?.latestDeployment?.user" tooltipPosition="top" />
<app-user-avatar [user]="environment?.latestDeployment?.user" tooltipPosition="top"/>
@if (environment.latestDeployment.user?.name) {
{{ environment.latestDeployment.user?.name }} deployed
}
@if (environment.latestDeployment.updatedAt) {
<span [pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
<span
[pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
}
<app-deployment-state-tag [state]="deployment.state" />
<p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center" [pTooltip]="tooltipTemplate" tooltipPosition="top">
<app-deployment-state-tag [state]="deployment.state"/>
<p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center"
[pTooltip]="tooltipTemplate" tooltipPosition="top">
<i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
<span class="ml-1 truncate flex-1">{{ deployment.ref }}</span>
<ng-template #tooltipTemplate>
Expand All @@ -59,13 +65,14 @@

@if (environment.locked) {
<div class="flex gap-1 items-center">
<app-user-avatar [user]="environment.lockedBy" [toolTipText]="'Locked By'" tooltipPosition="top" />
<app-user-avatar [user]="environment.lockedBy" [toolTipText]="'Locked By'" tooltipPosition="top"/>
</div>
@if (environment.lockedAt) {
<app-lock-time [timeLockWillExpire]="environment.lockWillExpireAt"></app-lock-time>
}
@if (isLoggedIn() && (environment.lockReservationWillExpireAt !== null || isCurrentUserLocked(environment) || hasUnlockPermissions())) {
<span pTooltip="{{ unlockToolTip(environment) }}" tooltipPosition="top" [ngStyle]="{ display: 'inline-block' }">
<span pTooltip="{{ unlockToolTip(environment) }}" tooltipPosition="top"
[ngStyle]="{ display: 'inline-block' }">
<button
(click)="onUnlockEnvironment($event, environment)"
class="p-button p-button-danger p-2"
Expand All @@ -75,30 +82,43 @@
cursor: canUnlock(environment) ? 'pointer' : 'not-allowed',
}"
>
<i-tabler name="lock-open" class="mr-1" />Unlock
<i-tabler name="lock-open" class="mr-1"/>Unlock
</button>
</span>
}
}

@if (userCanDeploy(environment)) {
<button (click)="deployEnvironment(environment); $event.stopPropagation()" class="p-button p-button-secondary p-2">
<i-tabler name="cloud-upload" class="mr-1 flex-shrink-0" />Deploy
</button>
<div class="flex items-center">
<button class="p-button p-button-secondary p-2"
(click)="deployEnvironment(environment); $event.stopPropagation()"
[disabled]="!userCanDeploy(environment)">
<i-tabler name="cloud-upload" class="mr-1 flex-shrink-0"/>
Deploy
</button>
@if (!environment.locked) {
<button class="p-button p-button-secondary p-2"
(click)="lockEnvironment(environment); $event.stopPropagation()"
[disabled]="!userCanDeploy(environment)">
<i-tabler name="lock" class="mr-1 flex-shrink-0"/>
</button>
}
</div>
}

@if (canViewAllEnvironments()) {
<!-- Show the disabled tag -->
@if (!environment.enabled) {
<p-tag value="Disabled" severity="danger" rounded="true" />
<p-tag value="Disabled" severity="danger" rounded="true"/>
}
<a
icon
[routerLink]="'/repo/' + environment.repository?.id + '/environment/' + environment.id + '/edit'"
class="p-button p-button-secondary p-2"
(click)="$event.stopPropagation()"
><i-tabler name="pencil"
/></a>
>
<i-tabler name="pencil"/>
</a>
}
<span class="w-2"></span>
</div>
Expand All @@ -108,26 +128,27 @@
<!-- Toggle Button -->
<div class="flex justify-end gap-2">
<span class="text-gray-500">Details</span>
<p-toggleswitch [ngModel]="showLatestDeployment" (click)="showLatestDeployment = !showLatestDeployment" />
<p-toggleswitch [ngModel]="showLatestDeployment" (click)="showLatestDeployment = !showLatestDeployment"/>
<span class="text-gray-500">Deployment</span>
</div>

<!-- Conditionally display information based on the toggle -->
@if (showLatestDeployment) {
<div class="flex w-full">
<app-deployment-stepper [deployment]="environment.latestDeployment" class="w-full" />
<app-deployment-stepper [deployment]="environment.latestDeployment" class="w-full"/>
</div>
} @else {
<div class="flex w-full">
<div class="flex flex-col gap-1">
@if (environment.latestDeployment; as deployment) {
<app-environment-deployment-info class="max-w-2xl" [deployment]="deployment" [repositoryId]="environment.repository?.id || 0" />
<app-environment-deployment-info class="max-w-2xl" [deployment]="deployment"
[repositoryId]="environment.repository?.id || 0"/>
}
</div>
<div class="flex-grow"></div>
<div class="flex flex-col gap-1">
@if (environment.latestStatus; as status) {
<app-environment-status-info class="max-w-xs mt-2 ml-14" [status]="status" />
<app-environment-status-info class="max-w-xs mt-2 ml-14" [status]="status"/>
}
</div>
</div>
Expand All @@ -141,7 +162,7 @@
[routerLink]="'/repo/' + environment.repository?.id + '/environment/' + environment.id + '/history'"
class="p-button p-button-text text-gray-500 py-2 flex items-center"
>
<i-tabler class="mr-1" name="history" />
<i-tabler class="mr-1" name="history"/>
View Activity History
</a>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Component, computed, inject, input, output, signal, OnDestroy } from '@angular/core';
import { Component, computed, inject, input, OnDestroy, output, signal } from '@angular/core';
import { AccordionModule } from 'primeng/accordion';

import { DatePipe, CommonModule } from '@angular/common';
import { CommonModule, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { EnvironmentDto } from '@app/core/modules/openapi';
import {
Expand All @@ -10,6 +9,7 @@ import {
getAllEnvironmentsOptions,
getAllEnvironmentsQueryKey,
getEnvironmentsByUserLockingQueryKey,
lockEnvironmentMutation,
unlockEnvironmentMutation,
} from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { PermissionService } from '@app/core/services/permission.service';
Expand Down Expand Up @@ -85,9 +85,18 @@ export class EnvironmentListViewComponent implements OnDestroy {
hasDeployPermissions = computed(() => this.permissionService.hasWritePermission());
hasEditEnvironmentPermissions = computed(() => this.permissionService.isAdmin());

lockEnvironmentMutation = injectMutation(() => ({
...lockEnvironmentMutation(),
onSuccess: () => {
this.queryClient.invalidateQueries({ queryKey: this.queryKey() });
this.queryClient.invalidateQueries({ queryKey: getEnvironmentsByUserLockingQueryKey() });
},
}));

userCanDeploy(environment: EnvironmentDto): boolean {
return !!(this.isLoggedIn() && this.deployable() && (!environment.locked || this.isCurrentUserLocked(environment)) && this.hasDeployPermissions());
}

deploy = output<EnvironmentDto>();

searchInput = signal<string>('');
Expand Down Expand Up @@ -128,6 +137,16 @@ export class EnvironmentListViewComponent implements OnDestroy {
},
}));

lockEnvironment(environment: EnvironmentDto) {
this.confirmationService.confirm({
header: 'Lock Environment',
message: `Are you sure you want to lock ${environment.name}?`,
accept: () => {
this.lockEnvironmentMutation.mutate({ path: { id: environment.id } });
},
});
}

constructor() {
this.intervalId = window.setInterval(() => {
this.currentTime.set(Date.now());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
GetEnvironmentByIdData,
UpdateEnvironmentData,
UnlockEnvironmentData,
LockEnvironmentData,
CreateWorkflowGroupData,
CreateWorkflowGroupResponse,
GetAllReleaseCandidatesData,
Expand Down Expand Up @@ -60,6 +61,7 @@ import {
getEnvironmentById,
updateEnvironment,
unlockEnvironment,
lockEnvironment,
createWorkflowGroup,
getAllReleaseCandidates,
createReleaseCandidate,
Expand Down Expand Up @@ -231,6 +233,20 @@ export const unlockEnvironmentMutation = (options?: Partial<Options<UnlockEnviro
return mutationOptions;
};

export const lockEnvironmentMutation = (options?: Partial<Options<LockEnvironmentData>>) => {
const mutationOptions: MutationOptions<unknown, DefaultError, Options<LockEnvironmentData>> = {
mutationFn: async localOptions => {
const { data } = await lockEnvironment({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

export const createWorkflowGroupQueryKey = (options: Options<CreateWorkflowGroupData>) => [createQueryKey('createWorkflowGroup', options)];

export const createWorkflowGroupOptions = (options: Options<CreateWorkflowGroupData>) => {
Expand Down
8 changes: 8 additions & 0 deletions client/src/app/core/modules/openapi/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
GetEnvironmentByIdResponse,
UpdateEnvironmentData,
UnlockEnvironmentData,
LockEnvironmentData,
CreateWorkflowGroupData,
CreateWorkflowGroupResponse,
GetAllReleaseCandidatesData,
Expand Down Expand Up @@ -152,6 +153,13 @@ export const unlockEnvironment = <ThrowOnError extends boolean = false>(options:
});
};

export const lockEnvironment = <ThrowOnError extends boolean = false>(options: Options<LockEnvironmentData, ThrowOnError>) => {
return (options?.client ?? client).put<unknown, unknown, ThrowOnError>({
...options,
url: '/api/environments/{id}/lock',
});
};

export const createWorkflowGroup = <ThrowOnError extends boolean = false>(options: Options<CreateWorkflowGroupData, ThrowOnError>) => {
return (options?.client ?? client).post<CreateWorkflowGroupResponse, unknown, ThrowOnError>({
...options,
Expand Down
16 changes: 16 additions & 0 deletions client/src/app/core/modules/openapi/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,22 @@ export type UnlockEnvironmentResponses = {
200: unknown;
};

export type LockEnvironmentData = {
body?: never;
path: {
id: number;
};
query?: never;
url: '/api/environments/{id}/lock';
};

export type LockEnvironmentResponses = {
/**
* OK
*/
200: unknown;
};

export type CreateWorkflowGroupData = {
body: WorkflowGroupDto;
path: {
Expand Down
18 changes: 18 additions & 0 deletions server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,24 @@ paths:
content:
application/json:
schema: {}
/api/environments/{id}/lock:
put:
tags:
- environment-controller
operationId: lockEnvironment
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
"200":
description: OK
content:
application/json:
schema: {}
/api/settings/{repositoryId}/groups/create:
post:
tags:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import de.tum.cit.aet.helios.auth.AuthService;
import de.tum.cit.aet.helios.branch.BranchService;
import de.tum.cit.aet.helios.environment.Environment;
import de.tum.cit.aet.helios.environment.EnvironmentDto;
import de.tum.cit.aet.helios.environment.EnvironmentLockHistory;
import de.tum.cit.aet.helios.environment.EnvironmentLockHistoryRepository;
import de.tum.cit.aet.helios.environment.EnvironmentRepository;
Expand Down Expand Up @@ -141,10 +142,15 @@ private Environment lockEnvironment(Long environmentId) {

// Only attempt to lock if it's a test environment
if (environment.getType() == Environment.Type.TEST) {
environment =
this.environmentService
.lockEnvironment(environmentId)
.orElseThrow(() -> new DeploymentException("Environment was already locked"));
EnvironmentDto environmentDto = this.environmentService.lockEnvironment(environmentId);
if (environmentDto == null) {
throw new DeploymentException("Failed to lock environment");
} else {
environment = environmentRepository.findById(environmentDto.id()).orElse(null);
if (environment == null) {
throw new DeploymentException("Failed to lock environment");
}
}
}

if (!canRedeploy(environment, 20)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public ResponseEntity<List<EnvironmentLockHistoryDto>> getLockHistoryByEnvironme
@PathVariable Long environmentId) {
List<EnvironmentLockHistoryDto> lockHistory =
environmentService.getLockHistoryByEnvironmentId(environmentId);

return ResponseEntity.ok(lockHistory);
}

Expand All @@ -70,6 +70,13 @@ public ResponseEntity<?> unlockEnvironment(@PathVariable Long id) {
return ResponseEntity.ok(environment);
}

@EnforceAtLeastWritePermission
@PutMapping("/{id}/lock")
public ResponseEntity<?> lockEnvironment(@PathVariable Long id) {
EnvironmentDto environment = environmentService.lockEnvironment(id);
return ResponseEntity.ok(environment);
}

@EnforceAtLeastMaintainer
@PutMapping("/{id}")
public ResponseEntity<?> updateEnvironment(
Expand Down
Loading
Loading