Skip to content

Commit

Permalink
Feat/i want to duplicate a stack (#110)
Browse files Browse the repository at this point in the history
* feat(duplication): copy main relations

* fix(stack): duplicate links

* feat(stack): duplicate in front

* feat(stack): remove print
  • Loading branch information
RomainDreidemy authored Sep 8, 2023
1 parent cdf003c commit 6a26a24
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 5 deletions.
31 changes: 31 additions & 0 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,37 @@ const docTemplate = `{
}
}
},
"/stacks/{id}/duplicate": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Stacks"
],
"summary": "Duplicate a stack",
"parameters": [
{
"type": "string",
"description": "Stack ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.StackResponse"
}
}
}
}
},
"/stacks/{stackId}/board": {
"get": {
"consumes": [
Expand Down
31 changes: 31 additions & 0 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,37 @@
}
}
},
"/stacks/{id}/duplicate": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Stacks"
],
"summary": "Duplicate a stack",
"parameters": [
{
"type": "string",
"description": "Stack ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.StackResponse"
}
}
}
}
},
"/stacks/{stackId}/board": {
"get": {
"consumes": [
Expand Down
20 changes: 20 additions & 0 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,26 @@ paths:
summary: Update a stack
tags:
- Stacks
/stacks/{id}/duplicate:
post:
consumes:
- application/json
parameters:
- description: Stack ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.StackResponse'
summary: Duplicate a stack
tags:
- Stacks
/stacks/{stackId}/board:
get:
consumes:
Expand Down
24 changes: 24 additions & 0 deletions api/src/controllers/stack_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/RomainDreidemy/MT5-docker-extension/src/models"
"github.com/RomainDreidemy/MT5-docker-extension/src/policies"
"github.com/RomainDreidemy/MT5-docker-extension/src/repositories"
"github.com/RomainDreidemy/MT5-docker-extension/src/services/duplication"
"github.com/RomainDreidemy/MT5-docker-extension/src/services/factories"
"github.com/gofiber/fiber/v2"
)
Expand Down Expand Up @@ -141,3 +142,26 @@ func DeleteStack(c *fiber.Ctx) error {

return c.Status(fiber.StatusNoContent).Send(nil)
}

// DuplicateStack godoc
// @Summary Duplicate a stack
// @Tags Stacks
// @Accept json
// @Produce json
// @Param id path string true "Stack ID"
// @Success 200 {object} models.StackResponse
// @Router /stacks/{id}/duplicate [post]
func DuplicateStack(c *fiber.Ctx) error {
id := c.Params("id")
currentUser := c.Locals("user").(models.UserResponse)

if !policies.CanAccessStack(currentUser, id) {
return c.Status(fiber.StatusNotFound).JSON(factories.BuildErrorResponse("error", "Stack not found"))
}

stack, _ := repositories.FindStackWithAssociations(id)

duplicatedStack := duplication.DuplicateStack(stack)

return c.Status(fiber.StatusCreated).JSON(factories.BuildStackResponse(duplicatedStack))
}
2 changes: 1 addition & 1 deletion api/src/middlewares/deserialize_user_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func DeserializeUser(c *fiber.Ctx) error {
var user models.User
initializers.DB.First(&user, "id = ?", fmt.Sprint(claims["sub"]))

if user.ID.String() != claims["sub"] {
if user.ID == nil || user.ID.String() != claims["sub"] {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"status": "fail", "message": "the user belonging to this token no logger exists"})
}

Expand Down
16 changes: 16 additions & 0 deletions api/src/repositories/stack_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ func FindStack(stackId string) (models.Stack, *gorm.DB) {
return stack, result
}

func FindStackWithAssociations(stackId string) (models.Stack, *gorm.DB) {
var stack models.Stack
result := initializers.DB.
Preload("Services").
Preload("Services.ServiceVolumes").
Preload("Services.ServiceEnvVariables").
Preload("Services.ServicePorts").
Preload("Services.ServiceNetworkLinks").
Preload("Services.ServiceManagedVolumeLinks").
Preload("Networks").
Preload("ManagedVolumes").
First(&stack, "id = ?", stackId)

return stack, result
}

func GetStackByIdForAUser(stackId string, userId uuid.UUID) (*gorm.DB, models.Stack) {
var stack models.Stack
result := initializers.DB.First(&stack, "id = ? and user_id = ?", stackId, userId)
Expand Down
1 change: 1 addition & 0 deletions api/src/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func main() {
stacks.Post("/", controllers.CreateStack)
stacks.Put("/:id", controllers.UpdateStack)
stacks.Delete("/:id", controllers.DeleteStack)
stacks.Post("/:id/duplicate", controllers.DuplicateStack)

stacks.Post("/:stackId/services", controllers.CreateService)
stacks.Get("/:stackId/services", controllers.GetServices)
Expand Down
148 changes: 148 additions & 0 deletions api/src/services/duplication/duplication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package duplication

import (
"github.com/RomainDreidemy/MT5-docker-extension/src/models"
"github.com/RomainDreidemy/MT5-docker-extension/src/repositories"
)

func DuplicateStack(stack models.Stack) models.Stack {
newStack := models.Stack{
UserID: stack.UserID,
Name: "Copy of " + stack.Name,
Description: stack.Description,
}

repositories.Create[models.Stack](&newStack)

serviceMapping := make(map[string]string)
networkMapping := make(map[string]string)
volumeMapping := make(map[string]string)

for _, service := range stack.Services {
newService := duplicateService(service, newStack.ID.String())
repositories.Create[models.Service](&newService)
newServiceId := newService.ID.String()
serviceMapping[service.ID.String()] = newServiceId

duplicateServiceEnvVariables(service.ServiceEnvVariables, newServiceId)
duplicateServicePorts(service.ServicePorts, newServiceId)
duplicateServiceVolumes(service.ServiceVolumes, newServiceId)
}

for _, network := range stack.Networks {
newNetwork := models.Network{
StackID: newStack.ID.String(),
Name: network.Name,
Description: network.Description,
IsExternal: network.IsExternal,
PositionX: network.PositionX,
PositionY: network.PositionY,
Driver: network.Driver,
}

repositories.Create[models.Network](&newNetwork)
networkMapping[network.ID.String()] = newNetwork.ID.String()
}

for _, volume := range stack.ManagedVolumes {
newVolume := models.ManagedVolume{
StackID: newStack.ID.String(),
Name: volume.Name,
Description: volume.Description,
PositionX: volume.PositionX,
PositionY: volume.PositionY,
}

repositories.Create[models.ManagedVolume](&newVolume)
volumeMapping[volume.ID.String()] = newVolume.ID.String()
}

for _, service := range stack.Services {
links := make([]models.ServiceNetworkLink, 0)
for _, link := range service.ServiceNetworkLinks {
newLink := models.ServiceNetworkLink{
ServiceID: serviceMapping[link.ServiceID],
NetworkID: networkMapping[link.NetworkID],
ServiceArrowPosition: link.ServiceArrowPosition,
NetworkArrowPosition: link.NetworkArrowPosition,
}

links = append(links, newLink)
}

repositories.Create[[]models.ServiceNetworkLink](&links)

volumeLinks := make([]models.ServiceManagedVolumeLink, 0)
for _, link := range service.ServiceManagedVolumeLinks {
newLink := models.ServiceManagedVolumeLink{
ServiceID: serviceMapping[link.ServiceID],
ManagedVolumeID: volumeMapping[link.ManagedVolumeID],
ServiceArrowPosition: link.ServiceArrowPosition,
ManagedVolumeArrowPosition: link.ManagedVolumeArrowPosition,
ContainerPath: link.ContainerPath,
}

volumeLinks = append(volumeLinks, newLink)
}

repositories.Create[[]models.ServiceManagedVolumeLink](&volumeLinks)
}

return newStack
}

func duplicateService(service models.Service, stackId string) models.Service {
return models.Service{
StackID: stackId,
Name: service.Name,
Description: service.Description,
DockerImage: service.DockerImage,
DockerTag: service.DockerTag,
Entrypoint: service.Entrypoint,
Context: service.Context,
Dockerfile: service.Dockerfile,
PositionX: service.PositionX,
PositionY: service.PositionY,
}
}

func duplicateServiceEnvVariables(environmentVariables []models.ServiceEnvVariable, serviceId string) {
envVariables := make([]models.ServiceEnvVariable, len(environmentVariables))
for _, environmentVariable := range environmentVariables {
envVariables = append(envVariables, models.ServiceEnvVariable{
ServiceID: serviceId,
Key: environmentVariable.Key,
Value: environmentVariable.Value,
})
}

repositories.Create[[]models.ServiceEnvVariable](&envVariables)
}

func duplicateServicePorts(ports []models.ServicePort, serviceId string) {
newPorts := make([]models.ServicePort, len(ports))

for _, port := range ports {
newPorts = append(newPorts, models.ServicePort{
ServiceID: serviceId,
Private: port.Private,
Public: port.Public,
})
}

repositories.Create[[]models.ServicePort](&newPorts)
}

func duplicateServiceVolumes(volumes []models.ServiceVolume, serviceId string) {
newVolumes := make([]models.ServiceVolume, len(volumes))

for _, volume := range volumes {
newVolumes = append(newVolumes, models.ServiceVolume{
ServiceID: serviceId,
LocalPath: volume.LocalPath,
ContainerPath: volume.ContainerPath,
})
}

repositories.Create[[]models.ServiceVolume](&newVolumes)
}
3 changes: 2 additions & 1 deletion front/src/services/entities/Stack.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const StackEntity = {
board: async (id: string): Promise<AxiosResponse<IBoard>> => await axios.get(`/stacks/${id}/board`),
create: async (stack: IStackCreate): Promise<AxiosResponse<IStack>> => await axios.post('/stacks', stack),
update: async (stack: IStack): Promise<AxiosResponse<IStack>> => await axios.put(`/stacks/${stack.id}`, stack),
delete: async (id: string): Promise<AxiosResponse> => await axios.delete(`/stacks/${id}`)
delete: async (id: string): Promise<AxiosResponse> => await axios.delete(`/stacks/${id}`),
duplicate: async (id: string): Promise<AxiosResponse<IStack>> => await axios.post(`/stacks/${id}/duplicate`)
}

export default StackEntity
7 changes: 5 additions & 2 deletions front/src/views/organisms/StackCard.organism.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import React from 'react'
import { Link } from 'react-router-dom'
import { type IStack } from '../../interfaces/Stack.interface'
import Button from '../atoms/forms/Button.atom'
import { BiTrash, BiEdit } from 'react-icons/bi'
import { BiTrash, BiEdit, BiCopy } from 'react-icons/bi'

const StackCardOrganism = ({ stack, id, name, description, onEdit, onDelete }: {
const StackCardOrganism = ({ stack, id, name, description, onEdit, onDelete, onDuplicate }: {
stack: IStack
id: string
name: string
description: string
onEdit: (stack: IStack) => void
onDelete: (id: string) => void
onDuplicate: (id: string) => void
}): JSX.Element => {
return (
<div className="card shadow-md mb-2 rounded border border-blue-100 hover:border-blue-200">
Expand All @@ -24,6 +25,8 @@ const StackCardOrganism = ({ stack, id, name, description, onEdit, onDelete }: {
<Button className="btn-ghost" icon={<BiTrash/>} onClick={() => { onDelete(id) }} />

<Button className="btn-ghost" icon={<BiEdit/>} onClick={() => { onEdit(stack) }} />

<Button className="btn-ghost" icon={<BiCopy/>} onClick={() => { onDuplicate(id) }} />
</div>
</div>
)
Expand Down
9 changes: 8 additions & 1 deletion front/src/views/pages/StacksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const StacksPage = (): JSX.Element => {
await getStacks()
}

const onDuplicate = async (id: string): Promise<void> => {
await StackEntity.duplicate(id)
await getStacks()
}

const formState = selectedStack == null ? 'create' : 'edit'
const FormComponent = formState === 'create'
? CreateStackFormModalOrganism
Expand Down Expand Up @@ -62,7 +67,9 @@ const StacksPage = (): JSX.Element => {
name={stack.name}
description={stack.description}
onEdit={() => { onOpenModal(stack) }}
onDelete={onDelete} />
onDelete={onDelete}
onDuplicate={onDuplicate}
/>
)))
}
</div>
Expand Down

0 comments on commit 6a26a24

Please sign in to comment.