From 66d7f7d513b60b909e0cba902ed23c285e103933 Mon Sep 17 00:00:00 2001 From: DoniLite Date: Sun, 6 Apr 2025 12:54:49 +0000 Subject: [PATCH 1/8] fix: db init service with Setup func --- .github/workflows/go.yml | 4 +-- controllers/auth_controller.go | 6 +++- db/db.go | 8 ++--- go.mod | 2 +- services/cloud_service.go | 66 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 services/cloud_service.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index dd29e92..e65988f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest services: docker: - image: docker:dind + image: docker:latest options: --privileged # Nécessaire pour exécuter Docker dans Docker steps: @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.22' + go-version: '1.23' - name: Start Docker Daemon run: | diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index e1135c1..ae281c3 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -18,6 +18,10 @@ type Claims struct { var JwtKey = []byte("your_secret_key") // Remplace par une clé secrète sécurisée func GenerateToken(c *gin.Context) { + dbx, err := db.Setup() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Some features not initialize properly"}) + } userID, ok := c.Get("userID") var user models.User if !ok { @@ -25,7 +29,7 @@ func GenerateToken(c *gin.Context) { return } - result := db.DB.First(&user, userID.(int)) + result := dbx.First(&user, userID.(int)) if result.Error != nil { c.JSON(http.StatusNotFound, gin.H{"message": "not found user"}) diff --git a/db/db.go b/db/db.go index d3a1c6d..ae4ce48 100644 --- a/db/db.go +++ b/db/db.go @@ -5,14 +5,12 @@ import ( "gorm.io/gorm" ) -var DB gorm.DB - // Contient la base de données du projet 'cloudbeast' gérée par `gorm ORM` -func init() { +func Setup() (*gorm.DB, error) { dsn := "host=localhost user=doni password=DoniLite13 dbname=anexis port=5432 sslmode=disable" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { - panic("failed to connect database") + return nil, err } // Migrate the schema @@ -35,5 +33,5 @@ func init() { // Delete - delete product // db.Delete(&product, 1) - DB = *db + return db, nil } diff --git a/go.mod b/go.mod index a111858..a2caa90 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module cloudbeast.doni/m -go 1.22.7 +go 1.23.5 toolchain go1.22.9 diff --git a/services/cloud_service.go b/services/cloud_service.go new file mode 100644 index 0000000..0d53640 --- /dev/null +++ b/services/cloud_service.go @@ -0,0 +1,66 @@ +package services + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/Backblaze/blazer/b2" +) + +func CopyFile(ctx context.Context, bucket *b2.Bucket, src, dst string) error { + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + obj := bucket.Object(dst) + w := obj.NewWriter(ctx) + if _, err := io.Copy(w, f); err != nil { + w.Close() + return err + } + return w.Close() +} + +func CopyLargeFile(ctx context.Context, bucket *b2.Bucket, writers int, src, dst string) error { + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + w := bucket.Object(dst).NewWriter(ctx) + w.ConcurrentUploads = writers + if _, err := io.Copy(w, f); err != nil { + w.Close() + return err + } + return w.Close() +} + +func DownloadFile(ctx context.Context, bucket *b2.Bucket, downloads int, src, dst string) error { + r := bucket.Object(src).NewReader(ctx) + defer r.Close() + + f, err := os.Create(dst) + if err != nil { + return err + } + r.ConcurrentDownloads = downloads + if _, err := io.Copy(f, r); err != nil { + f.Close() + return err + } + return f.Close() +} + +func PrintObjects(ctx context.Context, bucket *b2.Bucket) error { + iterator := bucket.List(ctx) + for iterator.Next() { + fmt.Println(iterator.Object()) + } + return iterator.Err() +} \ No newline at end of file From ecb1403117e891bf9df628e0b3a4872153f9c44f Mon Sep 17 00:00:00 2001 From: DoniLite Date: Sun, 13 Apr 2025 12:27:49 +0000 Subject: [PATCH 2/8] refactor: frontend remove SSR for a basic vite react app --- .encryption-service/service.log | 1 + Makefile | 2 +- api/routes.go | 5 +- cmd/main.go | 15 +- cmd/server_test.go | 17 +- package.json | 22 +- pnpm-lock.yaml | 1987 +++++++++++++++---------------- services/secureAlgo_service.go | 973 +++++++-------- vite.config.ts | 21 + 9 files changed, 1494 insertions(+), 1549 deletions(-) create mode 100644 vite.config.ts diff --git a/.encryption-service/service.log b/.encryption-service/service.log index 5ebb40f..d895355 100644 --- a/.encryption-service/service.log +++ b/.encryption-service/service.log @@ -2,3 +2,4 @@ {"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:22:37Z"} {"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:39:22Z"} {"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:40:14Z"} +{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-04-13T12:25:59Z"} diff --git a/Makefile b/Makefile index 141417c..050e8e4 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ migrate: # Commande pour tester le code test: @echo "Running tests..." - $(GO) test ./... + DOCKER_SOCKET_PATH=$(DOCKER_SOCKET_PATH) $(GO) test ./... # Commande pour afficher les variables d'environnement env: diff --git a/api/routes.go b/api/routes.go index 62ad546..597d43c 100644 --- a/api/routes.go +++ b/api/routes.go @@ -7,15 +7,14 @@ import ( "cloudbeast.doni/m/middleware" "cloudbeast.doni/m/routes" "github.com/gin-gonic/gin" - gossr "github.com/natewong1313/go-react-ssr" ) -func SetupRouter(router *gin.Engine, engine *gossr.Engine) { +func SetupRouter(router *gin.Engine) { router.GET("/ping", controllers.PingRoute) router.POST("/upload", controllers.UploadFile) router.GET("/file/:id", controllers.DownloadFile) router.GET("/staticFile/:file", routes.Static) - router.GET("/", routes.Index(engine)) + // router.GET("/", routes.Index(engine)) // Autres routes auth := router.Group("/auth") auth.Use(middleware.ValidateJWT) diff --git a/cmd/main.go b/cmd/main.go index a8fa911..997ad2a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,7 +9,6 @@ import ( "cloudbeast.doni/m/utils" "github.com/Backblaze/blazer/b2" "github.com/gin-gonic/gin" - gossr "github.com/natewong1313/go-react-ssr" "github.com/joho/godotenv" "github.com/gin-contrib/pprof" ) @@ -37,21 +36,9 @@ func main() { println(buckets) router := gin.Default() pprof.Register(router) - engineConfig := &gossr.Config{ - AssetRoute: "/static", - FrontendDir: "./frontend/src", - GeneratedTypesPath: "./frontend/src/generated.d.ts", - PropsStructsPath: "./models/props.go", - TailwindConfigPath: "./tailwind.config.js", - LayoutCSSFilePath: "input.css", - } - engine, enginErr := gossr.New(*engineConfig) - if enginErr != nil { - log.Fatal(enginErr) - } router.StaticFS("/static", gin.Dir("../client", false)) router.StaticFile("favicon.ico", "./client/ico.svg") - api.SetupRouter(router, engine) + api.SetupRouter(router) err = router.Run(":8080") // Utilisation du port 8080 pour HTTP if err != nil { diff --git a/cmd/server_test.go b/cmd/server_test.go index 2912c5d..375e606 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -7,28 +7,13 @@ import ( "cloudbeast.doni/m/api" "github.com/gin-gonic/gin" - gossr "github.com/natewong1313/go-react-ssr" "github.com/stretchr/testify/assert" ) func TestPingRoute(t *testing.T) { router := gin.Default() - engineConfig := &gossr.Config{ - AssetRoute: "/static", - FrontendDir: "../frontend/src", - GeneratedTypesPath: "../frontend/src/generated.d.ts", - PropsStructsPath: "../models/props.go", - TailwindConfigPath: "../tailwind.config.js", - LayoutCSSFilePath: "input.css", - } - engine, err := gossr.New(*engineConfig) - - if err != nil { - t.Fatal(err) - } - - api.SetupRouter(router, engine) + api.SetupRouter(router) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/ping", nil) diff --git a/package.json b/package.json index b1b1b8a..158c7cb 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,12 @@ "keywords": [], "author": "", "license": "ISC", + "packageManager": "pnpm@9.12.0+sha512.4abf725084d7bcbafbd728bfc7bee61f2f791f977fd87542b3579dcb23504d170d46337945e4c66485cd12d588a0c0e570ed9c477e7ccdd8507cf05f3f92eaca", "dependencies": { - "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "autoprefixer": "^10.4.20", - "daisyui": "^4.12.23", - "postcss": "^8.4.49", - "postcss-cli": "^11.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-scrollbar-hide": "^2.0.0", - "tailwindcss": "^3.4.17", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2" + "@tailwindcss/vite": "^4.1.3", + "@types/node": "^22.14.1", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.1.3", + "vite": "^6.2.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc2ae81..03a7e95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,394 +8,619 @@ importers: .: dependencies: - '@tailwindcss/aspect-ratio': - specifier: ^0.4.2 - version: 0.4.2(tailwindcss@3.4.17) - '@tailwindcss/forms': - specifier: ^0.5.9 - version: 0.5.9(tailwindcss@3.4.17) - '@tailwindcss/typography': - specifier: ^0.5.15 - version: 0.5.15(tailwindcss@3.4.17) - autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.4.49) - daisyui: - specifier: ^4.12.23 - version: 4.12.23(postcss@8.4.49) - postcss: - specifier: ^8.4.49 - version: 8.4.49 - postcss-cli: - specifier: ^11.0.0 - version: 11.0.0(jiti@1.21.7)(postcss@8.4.49) - react: - specifier: ^19.0.0 - version: 19.0.0 - react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) - tailwind-scrollbar-hide: - specifier: ^2.0.0 - version: 2.0.0(tailwindcss@3.4.17) + '@tailwindcss/vite': + specifier: ^4.1.3 + version: 4.1.3(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1)) + '@types/node': + specifier: ^22.14.1 + version: 22.14.1 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1)) tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - zustand: - specifier: ^5.0.2 - version: 5.0.2(@types/react@19.0.2)(react@19.0.0) - devDependencies: - '@types/react': - specifier: ^19.0.2 - version: 19.0.2 - '@types/react-dom': - specifier: ^19.0.2 - version: 19.0.2(@types/react@19.0.2) + specifier: ^4.1.3 + version: 4.1.3 + vite: + specifier: ^6.2.6 + version: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) packages: - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} + engines: {node: '>=6.9.0'} - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - - '@tailwindcss/aspect-ratio@0.4.2': - resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} peerDependencies: - tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' + '@babel/core': ^7.0.0 - '@tailwindcss/forms@0.5.9': - resolution: {integrity: sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==} - peerDependencies: - tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20' + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} - '@tailwindcss/typography@0.5.15': - resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} - '@types/react-dom@19.0.2': - resolution: {integrity: sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==} - peerDependencies: - '@types/react': ^19.0.0 + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} - '@types/react@19.0.2': - resolution: {integrity: sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + engines: {node: '>=6.9.0'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + '@babel/template@7.27.0': + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + engines: {node: '>=6.9.0'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 + '@esbuild/aix-ppc64@0.25.2': + resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + '@esbuild/android-arm64@0.25.2': + resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + '@esbuild/android-arm@0.25.2': + resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + '@esbuild/android-x64@0.25.2': + resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + '@esbuild/darwin-arm64@0.25.2': + resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] - browserslist@4.24.3: - resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + '@esbuild/darwin-x64@0.25.2': + resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} + '@esbuild/freebsd-arm64@0.25.2': + resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] - caniuse-lite@1.0.30001690: - resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} + '@esbuild/freebsd-x64@0.25.2': + resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + '@esbuild/linux-arm64@0.25.2': + resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.2': + resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + '@esbuild/linux-ia32@0.25.2': + resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + '@esbuild/linux-loong64@0.25.2': + resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} + '@esbuild/linux-mips64el@0.25.2': + resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + '@esbuild/linux-ppc64@0.25.2': + resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] - css-selector-tokenizer@0.8.0: - resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} + '@esbuild/linux-riscv64@0.25.2': + resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true + '@esbuild/linux-s390x@0.25.2': + resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + '@esbuild/linux-x64@0.25.2': + resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] - culori@3.3.0: - resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@esbuild/netbsd-arm64@0.25.2': + resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] - daisyui@4.12.23: - resolution: {integrity: sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ==} - engines: {node: '>=16.9.0'} + '@esbuild/netbsd-x64@0.25.2': + resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] - dependency-graph@0.11.0: - resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} - engines: {node: '>= 0.6.0'} + '@esbuild/openbsd-arm64@0.25.2': + resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + '@esbuild/openbsd-x64@0.25.2': + resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + '@esbuild/sunos-x64@0.25.2': + resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + '@esbuild/win32-arm64@0.25.2': + resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] - electron-to-chromium@1.5.76: - resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} + '@esbuild/win32-ia32@0.25.2': + resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + '@esbuild/win32-x64@0.25.2': + resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} - fastparse@1.1.2: - resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - fastq@1.18.0: - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + cpu: [arm] + os: [android] - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: '>=14'} + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + cpu: [arm64] + os: [android] - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + cpu: [arm64] + os: [darwin] - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + cpu: [x64] + os: [darwin] - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.1.3': + resolution: {integrity: sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==} + + '@tailwindcss/oxide-android-arm64@4.1.3': + resolution: {integrity: sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.3': + resolution: {integrity: sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==} + engines: {node: '>= 10'} + cpu: [arm64] os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + '@tailwindcss/oxide-darwin-x64@4.1.3': + resolution: {integrity: sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + '@tailwindcss/oxide-freebsd-x64@4.1.3': + resolution: {integrity: sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3': + resolution: {integrity: sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.3': + resolution: {integrity: sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.3': + resolution: {integrity: sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.3': + resolution: {integrity: sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.3': + resolution: {integrity: sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.3': + resolution: {integrity: sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.3': + resolution: {integrity: sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.3': + resolution: {integrity: sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.3': + resolution: {integrity: sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==} + peerDependencies: + vite: ^5.2.0 || ^6 - get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + '@types/node@22.14.1': + resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + caniuse-lite@1.0.30001713: + resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + electron-to-chromium@1.5.136: + resolution: {integrity: sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + esbuild@0.25.2: + resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} + engines: {node: '>=18'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true - mini-svg-data-uri@1.4.4: - resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} @@ -405,919 +630,653 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - postcss-cli@11.0.0: - resolution: {integrity: sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - postcss: ^8.0.0 - - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-load-config@5.1.0: - resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-reporter@7.1.0: - resolution: {integrity: sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==} - engines: {node: '>=10'} - peerDependencies: - postcss: ^8.1.0 - - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - pretty-hrtime@1.0.3: - resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} - engines: {node: '>= 0.8'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} - peerDependencies: - react: ^19.0.0 - - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} - engines: {node: '>=0.10.0'} - - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tailwind-scrollbar-hide@2.0.0: - resolution: {integrity: sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg==} - peerDependencies: - tailwindcss: '>=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20' - - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} - engines: {node: '>=14.0.0'} - hasBin: true - - thenby@1.3.4: - resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tailwindcss@4.1.3: + resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yaml@2.6.1: - resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} - engines: {node: '>= 14'} + vite@6.2.6: + resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - zustand@5.0.2: - resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==} - engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: - '@types/react': + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: optional: true - immer: + sass-embedded: optional: true - react: + stylus: optional: true - use-sync-external-store: + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: optional: true -snapshots: + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - '@alloc/quick-lru@5.2.0': {} + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 +snapshots: - '@jridgewell/gen-mapping@0.3.8': + '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': + '@babel/code-frame@7.26.2': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.18.0 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@sindresorhus/merge-streams@2.3.0': {} - - '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.17)': - dependencies: - tailwindcss: 3.4.17 - - '@tailwindcss/forms@0.5.9(tailwindcss@3.4.17)': - dependencies: - mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.17 + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@tailwindcss/typography@0.5.15(tailwindcss@3.4.17)': - dependencies: - lodash.castarray: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 - postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.17 + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@types/react-dom@19.0.2(@types/react@19.0.2)': + '@babel/generator@7.27.0': dependencies: - '@types/react': 19.0.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 - '@types/react@19.0.2': + '@babel/helper-compilation-targets@7.27.0': dependencies: - csstype: 3.1.3 - - ansi-regex@5.0.1: {} - - ansi-regex@6.1.0: {} + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 - ansi-styles@4.3.0: + '@babel/helper-module-imports@7.25.9': dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color - anymatch@3.1.3: + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color - arg@5.0.2: {} + '@babel/helper-plugin-utils@7.26.5': {} - autoprefixer@10.4.20(postcss@8.4.49): - dependencies: - browserslist: 4.24.3 - caniuse-lite: 1.0.30001690 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.4.49 - postcss-value-parser: 4.2.0 + '@babel/helper-string-parser@7.25.9': {} - balanced-match@1.0.2: {} + '@babel/helper-validator-identifier@7.25.9': {} - binary-extensions@2.3.0: {} + '@babel/helper-validator-option@7.25.9': {} - brace-expansion@2.0.1: + '@babel/helpers@7.27.0': dependencies: - balanced-match: 1.0.2 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 - braces@3.0.3: + '@babel/parser@7.27.0': dependencies: - fill-range: 7.1.1 + '@babel/types': 7.27.0 - browserslist@4.24.3: + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': dependencies: - caniuse-lite: 1.0.30001690 - electron-to-chromium: 1.5.76 - node-releases: 2.0.19 - update-browserslist-db: 1.1.1(browserslist@4.24.3) + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 - camelcase-css@2.0.1: {} - - caniuse-lite@1.0.30001690: {} - - chokidar@3.6.0: + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 - cliui@8.0.1: + '@babel/template@7.27.0': dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 - color-convert@2.0.1: + '@babel/traverse@7.27.0': dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@4.1.1: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color - css-selector-tokenizer@0.8.0: + '@babel/types@7.27.0': dependencies: - cssesc: 3.0.0 - fastparse: 1.1.2 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - cssesc@3.0.0: {} + '@esbuild/aix-ppc64@0.25.2': + optional: true - csstype@3.1.3: {} + '@esbuild/android-arm64@0.25.2': + optional: true - culori@3.3.0: {} + '@esbuild/android-arm@0.25.2': + optional: true - daisyui@4.12.23(postcss@8.4.49): - dependencies: - css-selector-tokenizer: 0.8.0 - culori: 3.3.0 - picocolors: 1.1.1 - postcss-js: 4.0.1(postcss@8.4.49) - transitivePeerDependencies: - - postcss + '@esbuild/android-x64@0.25.2': + optional: true - dependency-graph@0.11.0: {} + '@esbuild/darwin-arm64@0.25.2': + optional: true - didyoumean@1.2.2: {} + '@esbuild/darwin-x64@0.25.2': + optional: true - dlv@1.1.3: {} + '@esbuild/freebsd-arm64@0.25.2': + optional: true - eastasianwidth@0.2.0: {} + '@esbuild/freebsd-x64@0.25.2': + optional: true - electron-to-chromium@1.5.76: {} + '@esbuild/linux-arm64@0.25.2': + optional: true - emoji-regex@8.0.0: {} + '@esbuild/linux-arm@0.25.2': + optional: true - emoji-regex@9.2.2: {} + '@esbuild/linux-ia32@0.25.2': + optional: true - escalade@3.2.0: {} + '@esbuild/linux-loong64@0.25.2': + optional: true - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 + '@esbuild/linux-mips64el@0.25.2': + optional: true - fastparse@1.1.2: {} + '@esbuild/linux-ppc64@0.25.2': + optional: true - fastq@1.18.0: - dependencies: - reusify: 1.0.4 + '@esbuild/linux-riscv64@0.25.2': + optional: true - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 + '@esbuild/linux-s390x@0.25.2': + optional: true - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 + '@esbuild/linux-x64@0.25.2': + optional: true - fraction.js@4.3.7: {} + '@esbuild/netbsd-arm64@0.25.2': + optional: true - fs-extra@11.2.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 + '@esbuild/netbsd-x64@0.25.2': + optional: true - fsevents@2.3.3: + '@esbuild/openbsd-arm64@0.25.2': optional: true - function-bind@1.1.2: {} + '@esbuild/openbsd-x64@0.25.2': + optional: true - get-caller-file@2.0.5: {} + '@esbuild/sunos-x64@0.25.2': + optional: true - get-stdin@9.0.0: {} + '@esbuild/win32-arm64@0.25.2': + optional: true - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 + '@esbuild/win32-ia32@0.25.2': + optional: true - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 + '@esbuild/win32-x64@0.25.2': + optional: true - glob@10.4.5: - dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - globby@14.0.2: + '@jridgewell/gen-mapping@0.3.8': dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 - ignore: 5.3.2 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 - graceful-fs@4.2.11: {} + '@jridgewell/resolve-uri@3.1.2': {} - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 + '@jridgewell/set-array@1.2.1': {} - ignore@5.3.2: {} + '@jridgewell/sourcemap-codec@1.5.0': {} - is-binary-path@2.1.0: + '@jridgewell/trace-mapping@0.3.25': dependencies: - binary-extensions: 2.3.0 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 + '@rollup/rollup-android-arm-eabi@4.40.0': + optional: true - is-extglob@2.1.1: {} + '@rollup/rollup-android-arm64@4.40.0': + optional: true - is-fullwidth-code-point@3.0.0: {} + '@rollup/rollup-darwin-arm64@4.40.0': + optional: true - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 + '@rollup/rollup-darwin-x64@4.40.0': + optional: true - is-number@7.0.0: {} + '@rollup/rollup-freebsd-arm64@4.40.0': + optional: true - isexe@2.0.0: {} + '@rollup/rollup-freebsd-x64@4.40.0': + optional: true - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + optional: true - jiti@1.21.7: {} + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + optional: true - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 + '@rollup/rollup-linux-arm64-gnu@4.40.0': + optional: true - lilconfig@3.1.3: {} + '@rollup/rollup-linux-arm64-musl@4.40.0': + optional: true - lines-and-columns@1.2.4: {} + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + optional: true - lodash.castarray@4.4.0: {} + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + optional: true - lodash.isplainobject@4.0.6: {} + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + optional: true - lodash.merge@4.6.2: {} + '@rollup/rollup-linux-riscv64-musl@4.40.0': + optional: true - lru-cache@10.4.3: {} + '@rollup/rollup-linux-s390x-gnu@4.40.0': + optional: true - merge2@1.4.1: {} + '@rollup/rollup-linux-x64-gnu@4.40.0': + optional: true - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + '@rollup/rollup-linux-x64-musl@4.40.0': + optional: true - mini-svg-data-uri@1.4.4: {} + '@rollup/rollup-win32-arm64-msvc@4.40.0': + optional: true - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 + '@rollup/rollup-win32-ia32-msvc@4.40.0': + optional: true - minipass@7.1.2: {} + '@rollup/rollup-win32-x64-msvc@4.40.0': + optional: true - mz@2.7.0: + '@tailwindcss/node@4.1.3': dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.8: {} - - node-releases@2.0.19: {} + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.3 - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - object-assign@4.1.1: {} + '@tailwindcss/oxide-android-arm64@4.1.3': + optional: true - object-hash@3.0.0: {} + '@tailwindcss/oxide-darwin-arm64@4.1.3': + optional: true - package-json-from-dist@1.0.1: {} + '@tailwindcss/oxide-darwin-x64@4.1.3': + optional: true - path-key@3.1.1: {} + '@tailwindcss/oxide-freebsd-x64@4.1.3': + optional: true - path-parse@1.0.7: {} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3': + optional: true - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 + '@tailwindcss/oxide-linux-arm64-gnu@4.1.3': + optional: true - path-type@5.0.0: {} + '@tailwindcss/oxide-linux-arm64-musl@4.1.3': + optional: true - picocolors@1.1.1: {} + '@tailwindcss/oxide-linux-x64-gnu@4.1.3': + optional: true - picomatch@2.3.1: {} + '@tailwindcss/oxide-linux-x64-musl@4.1.3': + optional: true - pify@2.3.0: {} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.3': + optional: true - pirates@4.0.6: {} + '@tailwindcss/oxide-win32-x64-msvc@4.1.3': + optional: true - postcss-cli@11.0.0(jiti@1.21.7)(postcss@8.4.49): - dependencies: - chokidar: 3.6.0 - dependency-graph: 0.11.0 - fs-extra: 11.2.0 - get-stdin: 9.0.0 - globby: 14.0.2 - picocolors: 1.1.1 - postcss: 8.4.49 - postcss-load-config: 5.1.0(jiti@1.21.7)(postcss@8.4.49) - postcss-reporter: 7.1.0(postcss@8.4.49) - pretty-hrtime: 1.0.3 - read-cache: 1.0.0 - slash: 5.1.0 - yargs: 17.7.2 - transitivePeerDependencies: - - jiti - - tsx + '@tailwindcss/oxide@4.1.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.3 + '@tailwindcss/oxide-darwin-arm64': 4.1.3 + '@tailwindcss/oxide-darwin-x64': 4.1.3 + '@tailwindcss/oxide-freebsd-x64': 4.1.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.3 + '@tailwindcss/oxide-linux-x64-musl': 4.1.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.3 - postcss-import@15.1.0(postcss@8.4.49): + '@tailwindcss/vite@4.1.3(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1))': dependencies: - postcss: 8.4.49 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 + '@tailwindcss/node': 4.1.3 + '@tailwindcss/oxide': 4.1.3 + tailwindcss: 4.1.3 + vite: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) - postcss-js@4.0.1(postcss@8.4.49): + '@types/babel__core@7.20.5': dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.49 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 - postcss-load-config@4.0.2(postcss@8.4.49): + '@types/babel__generator@7.27.0': dependencies: - lilconfig: 3.1.3 - yaml: 2.6.1 - optionalDependencies: - postcss: 8.4.49 + '@babel/types': 7.27.0 - postcss-load-config@5.1.0(jiti@1.21.7)(postcss@8.4.49): + '@types/babel__template@7.4.4': dependencies: - lilconfig: 3.1.3 - yaml: 2.6.1 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.4.49 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 - postcss-nested@6.2.0(postcss@8.4.49): + '@types/babel__traverse@7.20.7': dependencies: - postcss: 8.4.49 - postcss-selector-parser: 6.1.2 + '@babel/types': 7.27.0 - postcss-reporter@7.1.0(postcss@8.4.49): - dependencies: - picocolors: 1.1.1 - postcss: 8.4.49 - thenby: 1.3.4 + '@types/estree@1.0.7': {} - postcss-selector-parser@6.0.10: + '@types/node@22.14.1': dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 + undici-types: 6.21.0 - postcss-selector-parser@6.1.2: + '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1))': dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) + transitivePeerDependencies: + - supports-color - postcss@8.4.49: + browserslist@4.24.4: dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 + caniuse-lite: 1.0.30001713 + electron-to-chromium: 1.5.136 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) - pretty-hrtime@1.0.3: {} + caniuse-lite@1.0.30001713: {} - queue-microtask@1.2.3: {} + convert-source-map@2.0.0: {} - react-dom@19.0.0(react@19.0.0): + debug@4.4.0: dependencies: - react: 19.0.0 - scheduler: 0.25.0 + ms: 2.1.3 - react@19.0.0: {} + detect-libc@2.0.3: {} - read-cache@1.0.0: - dependencies: - pify: 2.3.0 + electron-to-chromium@1.5.136: {} - readdirp@3.6.0: + enhanced-resolve@5.18.1: dependencies: - picomatch: 2.3.1 + graceful-fs: 4.2.11 + tapable: 2.2.1 - require-directory@2.1.1: {} + esbuild@0.25.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.2 + '@esbuild/android-arm': 0.25.2 + '@esbuild/android-arm64': 0.25.2 + '@esbuild/android-x64': 0.25.2 + '@esbuild/darwin-arm64': 0.25.2 + '@esbuild/darwin-x64': 0.25.2 + '@esbuild/freebsd-arm64': 0.25.2 + '@esbuild/freebsd-x64': 0.25.2 + '@esbuild/linux-arm': 0.25.2 + '@esbuild/linux-arm64': 0.25.2 + '@esbuild/linux-ia32': 0.25.2 + '@esbuild/linux-loong64': 0.25.2 + '@esbuild/linux-mips64el': 0.25.2 + '@esbuild/linux-ppc64': 0.25.2 + '@esbuild/linux-riscv64': 0.25.2 + '@esbuild/linux-s390x': 0.25.2 + '@esbuild/linux-x64': 0.25.2 + '@esbuild/netbsd-arm64': 0.25.2 + '@esbuild/netbsd-x64': 0.25.2 + '@esbuild/openbsd-arm64': 0.25.2 + '@esbuild/openbsd-x64': 0.25.2 + '@esbuild/sunos-x64': 0.25.2 + '@esbuild/win32-arm64': 0.25.2 + '@esbuild/win32-ia32': 0.25.2 + '@esbuild/win32-x64': 0.25.2 - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + escalade@3.2.0: {} - reusify@1.0.4: {} + fsevents@2.3.3: + optional: true - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 + gensync@1.0.0-beta.2: {} - scheduler@0.25.0: {} + globals@11.12.0: {} - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 + graceful-fs@4.2.11: {} - shebang-regex@3.0.0: {} + jiti@2.4.2: {} - signal-exit@4.1.0: {} + js-tokens@4.0.0: {} - slash@5.1.0: {} + jsesc@3.1.0: {} - source-map-js@1.2.1: {} + json5@2.2.3: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 + lightningcss-darwin-arm64@1.29.2: + optional: true - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + lightningcss-darwin-x64@1.29.2: + optional: true - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 + lightningcss-freebsd-x64@1.29.2: + optional: true - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - commander: 4.1.1 - glob: 10.4.5 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 + lightningcss-linux-arm64-gnu@1.29.2: + optional: true - supports-preserve-symlinks-flag@1.0.0: {} + lightningcss-linux-arm64-musl@1.29.2: + optional: true - tailwind-scrollbar-hide@2.0.0(tailwindcss@3.4.17): - dependencies: - tailwindcss: 3.4.17 + lightningcss-linux-x64-gnu@1.29.2: + optional: true - tailwindcss@3.4.17: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.4.49 - postcss-import: 15.1.0(postcss@8.4.49) - postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49) - postcss-nested: 6.2.0(postcss@8.4.49) - postcss-selector-parser: 6.1.2 - resolve: 1.22.10 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node + lightningcss-linux-x64-musl@1.29.2: + optional: true - thenby@1.3.4: {} + lightningcss-win32-arm64-msvc@1.29.2: + optional: true - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 + lightningcss-win32-x64-msvc@1.29.2: + optional: true - thenify@3.3.1: + lightningcss@1.29.2: dependencies: - any-promise: 1.3.0 + detect-libc: 2.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 - to-regex-range@5.0.1: + lru-cache@5.1.1: dependencies: - is-number: 7.0.0 + yallist: 3.1.1 - ts-interface-checker@0.1.13: {} + ms@2.1.3: {} - unicorn-magic@0.1.0: {} + nanoid@3.3.8: {} - universalify@2.0.1: {} + node-releases@2.0.19: {} - update-browserslist-db@1.1.1(browserslist@4.24.3): + picocolors@1.1.1: {} + + postcss@8.5.3: dependencies: - browserslist: 4.24.3 - escalade: 3.2.0 + nanoid: 3.3.8 picocolors: 1.1.1 + source-map-js: 1.2.1 - util-deprecate@1.0.2: {} + react-refresh@0.14.2: {} - which@2.0.2: + rollup@4.40.0: dependencies: - isexe: 2.0.0 + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 + fsevents: 2.3.3 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 + semver@6.3.1: {} - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 + source-map-js@1.2.1: {} - y18n@5.0.8: {} + tailwindcss@4.1.3: {} - yaml@2.6.1: {} + tapable@2.2.1: {} - yargs-parser@21.1.1: {} + undici-types@6.21.0: {} - yargs@17.7.2: + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: - cliui: 8.0.1 + browserslist: 4.24.4 escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 + picocolors: 1.1.1 - zustand@5.0.2(@types/react@19.0.2)(react@19.0.0): + vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1): + dependencies: + esbuild: 0.25.2 + postcss: 8.5.3 + rollup: 4.40.0 optionalDependencies: - '@types/react': 19.0.2 - react: 19.0.0 + '@types/node': 22.14.1 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.2 + yaml: 2.6.1 + + yallist@3.1.1: {} + + yaml@2.6.1: + optional: true diff --git a/services/secureAlgo_service.go b/services/secureAlgo_service.go index d6d615c..bea111f 100644 --- a/services/secureAlgo_service.go +++ b/services/secureAlgo_service.go @@ -22,45 +22,45 @@ import ( // SecurityConfig définit les limites de sécurité type SecurityConfig struct { - MaxCPUs int64 `json:"max_cpus"` - MaxMemoryMB int64 `json:"max_memory_mb"` - MaxExecTime int `json:"max_exec_time_seconds"` - WorkingDir string `json:"working_dir"` - StorageKey []byte `json:"storage_key"` // Clé pour chiffrer les algorithmes stockés + MaxCPUs int64 `json:"max_cpus"` + MaxMemoryMB int64 `json:"max_memory_mb"` + MaxExecTime int `json:"max_exec_time_seconds"` + WorkingDir string `json:"working_dir"` + StorageKey []byte `json:"storage_key"` // Clé pour chiffrer les algorithmes stockés } // AlgorithmMetadata contient les informations sur l'algorithme fourni type AlgorithmMetadata struct { - Language string `json:"language"` - BuildCmd string `json:"build_cmd"` - RunCmd string `json:"run_cmd"` - EntryPoints map[string]string `json:"entry_points"` // "encrypt" et "decrypt" + Language string `json:"language"` + BuildCmd string `json:"build_cmd"` + RunCmd string `json:"run_cmd"` + EntryPoints map[string]string `json:"entry_points"` // "encrypt" et "decrypt" } // ValidationResult contient les résultats de la validation d'un algorithme type ValidationResult struct { - IsValid bool `json:"is_valid"` - Errors []string `json:"errors"` - Warnings []string `json:"warnings"` - Performance struct { - EncryptionTime time.Duration `json:"encryption_time"` - DecryptionTime time.Duration `json:"decryption_time"` - MemoryUsage int64 `json:"memory_usage"` - } `json:"performance"` + IsValid bool `json:"is_valid"` + Errors []string `json:"errors"` + Warnings []string `json:"warnings"` + Performance struct { + EncryptionTime time.Duration `json:"encryption_time"` + DecryptionTime time.Duration `json:"decryption_time"` + MemoryUsage int64 `json:"memory_usage"` + } `json:"performance"` } // SecureEncryptionService est une version sécurisée du service de chiffrement type SecureEncryptionService struct { - docker *client.Client - config SecurityConfig - storageDir string - keyStorage *KeyStorage + docker *client.Client + config SecurityConfig + storageDir string + keyStorage *KeyStorage } // KeyStorage gère le stockage sécurisé des clés type KeyStorage struct { - storageKey []byte - storageDir string + storageKey []byte + storageDir string } // Logger global du service @@ -68,546 +68,549 @@ var logger *logrus.Logger // NewSecureEncryptionService crée une nouvelle instance du service sécurisé func NewSecureEncryptionService(config SecurityConfig) (*SecureEncryptionService, error) { - docker, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return nil, fmt.Errorf("failed to create Docker client: %w", err) - } - - keyStorage, err := NewKeyStorage(config.StorageKey, filepath.Join(config.WorkingDir, "keys")) - if err != nil { - return nil, fmt.Errorf("failed to initialize key storage: %w", err) - } - - return &SecureEncryptionService{ - docker: docker, - config: config, - storageDir: config.WorkingDir, - keyStorage: keyStorage, - }, nil + docker, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client: %w", err) + } + + keyStorage, err := NewKeyStorage(config.StorageKey, filepath.Join(config.WorkingDir, "keys")) + if err != nil { + return nil, fmt.Errorf("failed to initialize key storage: %w", err) + } + + return &SecureEncryptionService{ + docker: docker, + config: config, + storageDir: config.WorkingDir, + keyStorage: keyStorage, + }, nil } // StoreAlgorithm stocke et valide un nouvel algorithme func (s *SecureEncryptionService) StoreAlgorithm(ctx context.Context, userID string, files map[string][]byte, metadata AlgorithmMetadata) error { - // Valider l'algorithme avant de le stocker - result, err := s.ValidateAlgorithm(ctx, files, metadata) - if err != nil { - return fmt.Errorf("validation failed: %w", err) - } - if !result.IsValid { - return fmt.Errorf("invalid algorithm: %v", result.Errors) - } - - // Chiffrer les fichiers de l'algorithme - encryptedFiles := make(map[string][]byte) - for name, content := range files { - encrypted, err := s.encryptData(content) - if err != nil { - return fmt.Errorf("failed to encrypt file %s: %w", name, err) - } - encryptedFiles[name] = encrypted - } - - // Stocker les fichiers chiffrés - algoPath := filepath.Join(s.storageDir, "algorithms", userID) - if err := os.MkdirAll(algoPath, 0700); err != nil { - return fmt.Errorf("failed to create algorithm directory: %w", err) - } - - for name, content := range encryptedFiles { - if err := os.WriteFile(filepath.Join(algoPath, name), content, 0600); err != nil { - return fmt.Errorf("failed to write file %s: %w", name, err) - } - } - - return nil + // Valider l'algorithme avant de le stocker + result, err := s.ValidateAlgorithm(ctx, files, metadata) + if err != nil { + return fmt.Errorf("validation failed: %w", err) + } + if !result.IsValid { + return fmt.Errorf("invalid algorithm: %v", result.Errors) + } + + // Chiffrer les fichiers de l'algorithme + encryptedFiles := make(map[string][]byte) + for name, content := range files { + encrypted, err := s.encryptData(content) + if err != nil { + return fmt.Errorf("failed to encrypt file %s: %w", name, err) + } + encryptedFiles[name] = encrypted + } + + // Stocker les fichiers chiffrés + algoPath := filepath.Join(s.storageDir, "algorithms", userID) + if err := os.MkdirAll(algoPath, 0700); err != nil { + return fmt.Errorf("failed to create algorithm directory: %w", err) + } + + for name, content := range encryptedFiles { + if err := os.WriteFile(filepath.Join(algoPath, name), content, 0600); err != nil { + return fmt.Errorf("failed to write file %s: %w", name, err) + } + } + + return nil } // ValidateAlgorithm vérifie qu'un algorithme respecte toutes les contraintes func (s *SecureEncryptionService) ValidateAlgorithm(ctx context.Context, files map[string][]byte, metadata AlgorithmMetadata) (*ValidationResult, error) { - result := &ValidationResult{IsValid: true} - - // Vérifier les fichiers requis - if _, ok := files["metadata.json"]; !ok { - result.Errors = append(result.Errors, "missing metadata.json") - result.IsValid = false - } - - // Créer un conteneur temporaire pour les tests - containerConfig := &container.Config{ - Image: s.getDockerImageForLanguage(metadata.Language), - Cmd: []string{"sh", "-c", metadata.BuildCmd}, - WorkingDir: "/app", - } - - hostConfig := &container.HostConfig{ - Resources: container.Resources{ - Memory: s.config.MaxMemoryMB * 1024 * 1024, - NanoCPUs: s.config.MaxCPUs * 1e9, - }, - } - - // Tester la compilation - if _, err := s.runInContainer(ctx, containerConfig, hostConfig, files); err != nil { - result.Errors = append(result.Errors, fmt.Sprintf("build failed: %v", err)) - result.IsValid = false - } - - // Tester les performances et la conformité - if result.IsValid { - testData := []byte("test data for validation") - startTime := time.Now() - - if err := s.runAlgorithmTest(ctx, "encrypt", testData, metadata); err != nil { - result.Errors = append(result.Errors, fmt.Sprintf("encryption test failed: %v", err)) - result.IsValid = false - } - - result.Performance.EncryptionTime = time.Since(startTime) - } - - return result, nil + result := &ValidationResult{IsValid: true} + + // Vérifier les fichiers requis + if _, ok := files["metadata.json"]; !ok { + result.Errors = append(result.Errors, "missing metadata.json") + result.IsValid = false + } + + // Créer un conteneur temporaire pour les tests + containerConfig := &container.Config{ + Image: s.getDockerImageForLanguage(metadata.Language), + Cmd: []string{"sh", "-c", metadata.BuildCmd}, + WorkingDir: "/app", + } + + hostConfig := &container.HostConfig{ + Resources: container.Resources{ + Memory: s.config.MaxMemoryMB * 1024 * 1024, + NanoCPUs: s.config.MaxCPUs * 1e9, + }, + } + + // Tester la compilation + if _, err := s.runInContainer(ctx, containerConfig, hostConfig, files); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("build failed: %v", err)) + result.IsValid = false + } + + // Tester les performances et la conformité + if result.IsValid { + testData := []byte("test data for validation") + startTime := time.Now() + + if err := s.runAlgorithmTest(ctx, "encrypt", testData, metadata); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("encryption test failed: %v", err)) + result.IsValid = false + } + + result.Performance.EncryptionTime = time.Since(startTime) + } + + return result, nil } // Encrypt chiffre les données de manière sécurisée func (s *SecureEncryptionService) Encrypt(ctx context.Context, userID string, data []byte) ([]byte, error) { - return s.runSecureOperation(ctx, userID, "encrypt", data) + return s.runSecureOperation(ctx, userID, "encrypt", data) } // Decrypt déchiffre les données de manière sécurisée func (s *SecureEncryptionService) Decrypt(ctx context.Context, userID string, data []byte) ([]byte, error) { - return s.runSecureOperation(ctx, userID, "decrypt", data) + return s.runSecureOperation(ctx, userID, "decrypt", data) } // runSecureOperation exécute une opération dans un conteneur isolé func (s *SecureEncryptionService) runSecureOperation(ctx context.Context, userID, operation string, data []byte) ([]byte, error) { - // Créer un contexte avec timeout - ctx, cancel := context.WithTimeout(ctx, time.Duration(s.config.MaxExecTime)*time.Second) - defer cancel() - - // Récupérer et déchiffrer l'algorithme - algoPath := filepath.Join(s.storageDir, "algorithms", userID) - files, err := s.loadAndDecryptAlgorithm(algoPath) - if err != nil { - return nil, fmt.Errorf("failed to load algorithm: %w", err) - } - - var metadata AlgorithmMetadata - if err := json.Unmarshal(files["metadata.json"], &metadata); err != nil { - return nil, fmt.Errorf("failed to parse metadata: %w", err) - } - - // Configurer le conteneur - containerConfig := &container.Config{ - Image: s.getDockerImageForLanguage(metadata.Language), - Cmd: []string{"sh", "-c", fmt.Sprintf(metadata.RunCmd, metadata.EntryPoints[operation])}, - WorkingDir: "/app", - } - - hostConfig := &container.HostConfig{ - Resources: container.Resources{ - Memory: s.config.MaxMemoryMB * 1024 * 1024, - NanoCPUs: s.config.MaxCPUs * 1e9, - }, - } - - // Exécuter l'opération - outputChan := make(chan []byte, 1) - errChan := make(chan error, 1) - - go func() { - output, err := s.runInContainer(ctx, containerConfig, hostConfig, map[string][]byte{ - "input": data, - }) - if err != nil { - errChan <- err - return - } - outputChan <- output - }() - - // Attendre le résultat ou le timeout - select { - case output := <-outputChan: - return output, nil - case err := <-errChan: - return nil, err - case <-ctx.Done(): - return nil, fmt.Errorf("operation timed out") - } + // Créer un contexte avec timeout + ctx, cancel := context.WithTimeout(ctx, time.Duration(s.config.MaxExecTime)*time.Second) + defer cancel() + + // Récupérer et déchiffrer l'algorithme + algoPath := filepath.Join(s.storageDir, "algorithms", userID) + files, err := s.loadAndDecryptAlgorithm(algoPath) + if err != nil { + return nil, fmt.Errorf("failed to load algorithm: %w", err) + } + + var metadata AlgorithmMetadata + if err := json.Unmarshal(files["metadata.json"], &metadata); err != nil { + return nil, fmt.Errorf("failed to parse metadata: %w", err) + } + + // Configurer le conteneur + containerConfig := &container.Config{ + Image: s.getDockerImageForLanguage(metadata.Language), + Cmd: []string{"sh", "-c", fmt.Sprintf(metadata.RunCmd, metadata.EntryPoints[operation])}, + WorkingDir: "/app", + } + + hostConfig := &container.HostConfig{ + Resources: container.Resources{ + Memory: s.config.MaxMemoryMB * 1024 * 1024, + NanoCPUs: s.config.MaxCPUs * 1e9, + }, + } + + // Exécuter l'opération + outputChan := make(chan []byte, 1) + errChan := make(chan error, 1) + + go func() { + output, err := s.runInContainer(ctx, containerConfig, hostConfig, map[string][]byte{ + "input": data, + }) + if err != nil { + errChan <- err + return + } + outputChan <- output + }() + + // Attendre le résultat ou le timeout + select { + case output := <-outputChan: + return output, nil + case err := <-errChan: + return nil, err + case <-ctx.Done(): + return nil, fmt.Errorf("operation timed out") + } } // Méthodes utilitaires pour le chiffrement du stockage func (s *SecureEncryptionService) encryptData(data []byte) ([]byte, error) { - block, err := aes.NewCipher(s.config.StorageKey) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - return gcm.Seal(nonce, nonce, data, nil), nil + block, err := aes.NewCipher(s.config.StorageKey) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil } func (s *SecureEncryptionService) decryptData(data []byte) ([]byte, error) { - block, err := aes.NewCipher(s.config.StorageKey) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, fmt.Errorf("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - return gcm.Open(nil, nonce, ciphertext, nil) + block, err := aes.NewCipher(s.config.StorageKey) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) } func (s *SecureEncryptionService) getDockerImageForLanguage(language string) string { - // Map des images Docker sécurisées pour chaque langage supporté - images := map[string]string{ - "python": "python:3.9-slim", - "node": "node:16-slim", - "ruby": "ruby:3.0-slim", - "go": "golang:1.17-alpine", - "java": "openjdk:11-jre-slim", - } - return images[language] + // Map des images Docker sécurisées pour chaque langage supporté + images := map[string]string{ + "python": "python:3.9-slim", + "node": "node:16-slim", + "ruby": "ruby:3.0-slim", + "go": "golang:1.17-alpine", + "java": "openjdk:11-jre-slim", + } + return images[language] } func NewKeyStorage(storageKey []byte, storageDir string) (*KeyStorage, error) { - if err := os.MkdirAll(storageDir, 0700); err != nil { - return nil, fmt.Errorf("failed to create key storage directory: %w", err) - } - - return &KeyStorage{ - storageKey: storageKey, - storageDir: storageDir, - }, nil + if err := os.MkdirAll(storageDir, 0700); err != nil { + return nil, fmt.Errorf("failed to create key storage directory: %w", err) + } + + return &KeyStorage{ + storageKey: storageKey, + storageDir: storageDir, + }, nil } // loadAndDecryptAlgorithm charge et déchiffre les fichiers d'un algorithme func (s *SecureEncryptionService) loadAndDecryptAlgorithm(algoPath string) (map[string][]byte, error) { - files := make(map[string][]byte) - - // Lire tous les fichiers du répertoire - entries, err := os.ReadDir(algoPath) - if err != nil { - return nil, fmt.Errorf("failed to read algorithm directory: %w", err) - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - // Lire le fichier chiffré - encryptedData, err := os.ReadFile(filepath.Join(algoPath, entry.Name())) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", entry.Name(), err) - } - - // Déchiffrer le contenu - decryptedData, err := s.decryptData(encryptedData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt file %s: %w", entry.Name(), err) - } - - files[entry.Name()] = decryptedData - } - - return files, nil + files := make(map[string][]byte) + + // Lire tous les fichiers du répertoire + entries, err := os.ReadDir(algoPath) + if err != nil { + return nil, fmt.Errorf("failed to read algorithm directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + // Lire le fichier chiffré + encryptedData, err := os.ReadFile(filepath.Join(algoPath, entry.Name())) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", entry.Name(), err) + } + + // Déchiffrer le contenu + decryptedData, err := s.decryptData(encryptedData) + if err != nil { + return nil, fmt.Errorf("failed to decrypt file %s: %w", entry.Name(), err) + } + + files[entry.Name()] = decryptedData + } + + return files, nil } // runInContainer exécute une commande dans un conteneur Docker func (s *SecureEncryptionService) runInContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, files map[string][]byte) ([]byte, error) { - // Créer le conteneur - resp, err := s.docker.ContainerCreate(ctx, config, hostConfig, nil, nil, "") - if err != nil { - return nil, fmt.Errorf("failed to create container: %w", err) - } - defer s.docker.ContainerRemove(context.Background(), resp.ID, container.RemoveOptions{Force: true}) - - // Copier les fichiers dans le conteneur - for name, content := range files { - err = s.copyToContainer(ctx, resp.ID, filepath.Join("/app", name), content) - if err != nil { - return nil, fmt.Errorf("failed to copy file to container: %w", err) - } - } - - // Démarrer le conteneur - if err := s.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { - return nil, fmt.Errorf("failed to start container: %w", err) - } - - // Attendre la fin de l'exécution - statusCh, errCh := s.docker.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) - select { - case err := <-errCh: - return nil, fmt.Errorf("error waiting for container: %w", err) - case status := <-statusCh: - if status.StatusCode != 0 { - return nil, fmt.Errorf("container exited with status code %d", status.StatusCode) - } - } - - // Récupérer la sortie - out, err := s.docker.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) - if err != nil { - return nil, fmt.Errorf("failed to get container logs: %w", err) - } - defer out.Close() - - return io.ReadAll(out) + // Créer le conteneur + resp, err := s.docker.ContainerCreate(ctx, config, hostConfig, nil, nil, "") + if err != nil { + return nil, fmt.Errorf("failed to create container: %w", err) + } + defer s.docker.ContainerRemove(context.Background(), resp.ID, container.RemoveOptions{Force: true}) + + // Copier les fichiers dans le conteneur + for name, content := range files { + err = s.copyToContainer(ctx, resp.ID, filepath.Join("/app", name), content) + if err != nil { + return nil, fmt.Errorf("failed to copy file to container: %w", err) + } + } + + // Démarrer le conteneur + if err := s.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return nil, fmt.Errorf("failed to start container: %w", err) + } + + // Attendre la fin de l'exécution + statusCh, errCh := s.docker.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + return nil, fmt.Errorf("error waiting for container: %w", err) + case status := <-statusCh: + if status.StatusCode != 0 { + return nil, fmt.Errorf("container exited with status code %d", status.StatusCode) + } + } + + // Récupérer la sortie + out, err := s.docker.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) + if err != nil { + return nil, fmt.Errorf("failed to get container logs: %w", err) + } + defer out.Close() + + return io.ReadAll(out) } // copyToContainer copie un fichier dans un conteneur func (s *SecureEncryptionService) copyToContainer(ctx context.Context, containerID, path string, content []byte) error { - // Créer une archive tar contenant le fichier - tarContent, err := createTarArchive(path, content) - if err != nil { - return fmt.Errorf("failed to create tar archive: %w", err) - } - - // Copier l'archive dans le conteneur - return s.docker.CopyToContainer(ctx, containerID, "/", tarContent, container.CopyToContainerOptions{}) + // Créer une archive tar contenant le fichier + tarContent, err := createTarArchive(path, content) + if err != nil { + return fmt.Errorf("failed to create tar archive: %w", err) + } + + // Copier l'archive dans le conteneur + return s.docker.CopyToContainer(ctx, containerID, "/", tarContent, container.CopyToContainerOptions{}) } // runAlgorithmTest teste l'exécution d'un algorithme func (s *SecureEncryptionService) runAlgorithmTest(ctx context.Context, operation string, testData []byte, metadata AlgorithmMetadata) error { - containerConfig := &container.Config{ - Image: s.getDockerImageForLanguage(metadata.Language), - Cmd: []string{"sh", "-c", fmt.Sprintf(metadata.RunCmd, metadata.EntryPoints[operation])}, - WorkingDir: "/app", - } - - hostConfig := &container.HostConfig{ - Resources: container.Resources{ - Memory: s.config.MaxMemoryMB * 1024 * 1024, - NanoCPUs: s.config.MaxCPUs * 1e9, - }, - } - - _, err := s.runInContainer(ctx, containerConfig, hostConfig, map[string][]byte{ - "input": testData, - }) - return err + containerConfig := &container.Config{ + Image: s.getDockerImageForLanguage(metadata.Language), + Cmd: []string{"sh", "-c", fmt.Sprintf(metadata.RunCmd, metadata.EntryPoints[operation])}, + WorkingDir: "/app", + } + + hostConfig := &container.HostConfig{ + Resources: container.Resources{ + Memory: s.config.MaxMemoryMB * 1024 * 1024, + NanoCPUs: s.config.MaxCPUs * 1e9, + }, + } + + _, err := s.runInContainer(ctx, containerConfig, hostConfig, map[string][]byte{ + "input": testData, + }) + return err } // createTarArchive crée une archive tar contenant un fichier func createTarArchive(path string, content []byte) (io.Reader, error) { - // Créer un buffer pour l'archive - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - - // Créer l'en-tête du fichier - header := &tar.Header{ - Name: path, - Size: int64(len(content)), - Mode: 0644, - ModTime: time.Now(), - } - - // Écrire l'en-tête - if err := tw.WriteHeader(header); err != nil { - return nil, err - } - - // Écrire le contenu - if _, err := tw.Write(content); err != nil { - return nil, err - } - - // Fermer l'archive - if err := tw.Close(); err != nil { - return nil, err - } - - return &buf, nil + // Créer un buffer pour l'archive + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + + // Créer l'en-tête du fichier + header := &tar.Header{ + Name: path, + Size: int64(len(content)), + Mode: 0644, + ModTime: time.Now(), + } + + // Écrire l'en-tête + if err := tw.WriteHeader(header); err != nil { + return nil, err + } + + // Écrire le contenu + if _, err := tw.Write(content); err != nil { + return nil, err + } + + // Fermer l'archive + if err := tw.Close(); err != nil { + return nil, err + } + + return &buf, nil } // ServiceOptions contient toutes les options de configuration du service type ServiceOptions struct { - // Configuration Docker - DockerHost string - DockerTLSVerify bool - DockerCertPath string - - // Limites de ressources - MaxCPUs int64 - MaxMemoryMB int64 - MaxExecTime int - - // Chemins et stockage - WorkingDir string - StorageKey []byte - LogPath string - LogLevel string - EnableMetrics bool + // Configuration Docker + DockerHost string + DockerTLSVerify bool + DockerCertPath string + + // Limites de ressources + MaxCPUs int64 + MaxMemoryMB int64 + MaxExecTime int + + // Chemins et stockage + WorkingDir string + StorageKey []byte + LogPath string + LogLevel string + EnableMetrics bool } - // InitService initialise le service de chiffrement sécurisé func InitService(opts *ServiceOptions) (*SecureEncryptionService, error) { - // Initialisation du logger - logger = initLogger(opts.LogPath, opts.LogLevel) - - // Validation des options - if err := validateOptions(opts); err != nil { - return nil, fmt.Errorf("invalid options: %w", err) - } - - // Configuration des variables d'environnement Docker - setDockerEnv(opts) - - // Création du client Docker - dockerClient, err := client.NewClientWithOpts( - client.FromEnv, - client.WithHost(opts.DockerHost), - ) - if err != nil { - return nil, fmt.Errorf("failed to create Docker client: %w", err) - } - - // Test de la connexion Docker - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if _, err := dockerClient.Ping(ctx); err != nil { - return nil, fmt.Errorf("failed to connect to Docker daemon: %w", err) - } - - // Création de la configuration du service - config := SecurityConfig{ - MaxCPUs: opts.MaxCPUs, - MaxMemoryMB: opts.MaxMemoryMB, - MaxExecTime: opts.MaxExecTime, - WorkingDir: opts.WorkingDir, - StorageKey: opts.StorageKey, - } - - // Initialisation du service - service, err := NewSecureEncryptionService(config) - if err != nil { - return nil, fmt.Errorf("failed to create secure encryption service: %w", err) - } - - // Initialisation des métriques si activées - if opts.EnableMetrics { - initMetrics() - } - - logger.Info("Secure encryption service initialized successfully") - return service, nil + // Initialisation du logger + logger = initLogger(opts.LogPath, opts.LogLevel) + + // Validation des options + if err := validateOptions(opts); err != nil { + return nil, fmt.Errorf("invalid options: %w", err) + } + + // Configuration des variables d'environnement Docker + setDockerEnv(opts) + + // Création du client Docker + dockerClient, err := client.NewClientWithOpts( + client.FromEnv, + client.WithHost(opts.DockerHost), + ) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client: %w", err) + } + + // Test de la connexion Docker + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if _, err := dockerClient.Ping(ctx); err != nil { + return nil, fmt.Errorf("failed to connect to Docker daemon: %w", err) + } + + // Création de la configuration du service + config := SecurityConfig{ + MaxCPUs: opts.MaxCPUs, + MaxMemoryMB: opts.MaxMemoryMB, + MaxExecTime: opts.MaxExecTime, + WorkingDir: opts.WorkingDir, + StorageKey: opts.StorageKey, + } + + // Initialisation du service + service, err := NewSecureEncryptionService(config) + if err != nil { + return nil, fmt.Errorf("failed to create secure encryption service: %w", err) + } + + // Initialisation des métriques si activées + if opts.EnableMetrics { + initMetrics() + } + + logger.Info("Secure encryption service initialized successfully") + return service, nil } - // initLogger initialise le système de journalisation func initLogger(logPath, logLevel string) *logrus.Logger { - logger := logrus.New() - - // Configuration du niveau de log - level, err := logrus.ParseLevel(logLevel) - if err != nil { - level = logrus.InfoLevel - } - logger.SetLevel(level) - - // Configuration du format - logger.SetFormatter(&logrus.JSONFormatter{ - TimestampFormat: time.RFC3339, - }) - - // Configuration de la sortie - if logPath != "" { - if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { - log.Printf("Failed to create log directory: %v", err) - return logger - } - - file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - log.Printf("Failed to open log file: %v", err) - return logger - } - logger.SetOutput(file) - } - - return logger + logger := logrus.New() + + // Configuration du niveau de log + level, err := logrus.ParseLevel(logLevel) + if err != nil { + level = logrus.InfoLevel + } + logger.SetLevel(level) + + // Configuration du format + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + }) + + // Configuration de la sortie + if logPath != "" { + if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { + log.Printf("Failed to create log directory: %v", err) + return logger + } + + file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + log.Printf("Failed to open log file: %v", err) + return logger + } + logger.SetOutput(file) + } + + return logger } + // DefaultServiceOptions retourne des options par défaut sécurisées func DefaultServiceOptions() *ServiceOptions { - // Générer une clé de stockage aléatoire - storageKey := make([]byte, 32) - if _, err := rand.Read(storageKey); err != nil { - panic(fmt.Sprintf("failed to generate storage key: %v", err)) - } - - return &ServiceOptions{ - DockerHost: "unix:///var/run/docker.sock", - DockerTLSVerify: true, - MaxCPUs: 2, - MaxMemoryMB: 512, - MaxExecTime: 30, - WorkingDir: "../.encryption-service", - StorageKey: storageKey, - LogPath: "../.encryption-service/service.log", - LogLevel: "info", - EnableMetrics: true, - } + // Générer une clé de stockage aléatoire + storageKey := make([]byte, 32) + if _, err := rand.Read(storageKey); err != nil { + panic(fmt.Sprintf("failed to generate storage key: %v", err)) + } + + socketPath := os.Getenv("DOCKER_SOCKET_PATH") + if socketPath == "" { + socketPath = "/var/run/docker.sock" // fallback par défaut + } + + return &ServiceOptions{ + DockerHost: "unix://" + socketPath, + DockerTLSVerify: true, + MaxCPUs: 2, + MaxMemoryMB: 512, + MaxExecTime: 30, + WorkingDir: "../.encryption-service", + StorageKey: storageKey, + LogPath: "../.encryption-service/service.log", + LogLevel: "info", + EnableMetrics: true, + } } // validateOptions vérifie la validité des options func validateOptions(opts *ServiceOptions) error { - if opts.MaxCPUs <= 0 { - return fmt.Errorf("MaxCPUs must be positive") - } - if opts.MaxMemoryMB <= 0 { - return fmt.Errorf("MaxMemoryMB must be positive") - } - if opts.MaxExecTime <= 0 { - return fmt.Errorf("MaxExecTime must be positive") - } - if opts.WorkingDir == "" { - return fmt.Errorf("WorkingDir must not be empty") - } - if len(opts.StorageKey) != 32 { - return fmt.Errorf("StorageKey must be 32 bytes") - } - return nil + if opts.MaxCPUs <= 0 { + return fmt.Errorf("MaxCPUs must be positive") + } + if opts.MaxMemoryMB <= 0 { + return fmt.Errorf("MaxMemoryMB must be positive") + } + if opts.MaxExecTime <= 0 { + return fmt.Errorf("MaxExecTime must be positive") + } + if opts.WorkingDir == "" { + return fmt.Errorf("WorkingDir must not be empty") + } + if len(opts.StorageKey) != 32 { + return fmt.Errorf("StorageKey must be 32 bytes") + } + return nil } // setDockerEnv configure les variables d'environnement Docker func setDockerEnv(opts *ServiceOptions) { - os.Setenv("DOCKER_HOST", opts.DockerHost) - if opts.DockerTLSVerify { - os.Setenv("DOCKER_TLS_VERIFY", "1") - } - if opts.DockerCertPath != "" { - os.Setenv("DOCKER_CERT_PATH", opts.DockerCertPath) - } + os.Setenv("DOCKER_HOST", opts.DockerHost) + if opts.DockerTLSVerify { + os.Setenv("DOCKER_TLS_VERIFY", "1") + } + if opts.DockerCertPath != "" { + os.Setenv("DOCKER_CERT_PATH", opts.DockerCertPath) + } } - // Métriques Prometheus (à implémenter selon vos besoins) func initMetrics() { - // TODO: Initialiser les métriques Prometheus - // Exemple de métriques à suivre : - // - Nombre d'opérations de chiffrement/déchiffrement - // - Temps d'exécution des opérations - // - Utilisation des ressources - // - Taux d'erreur + // TODO: Initialiser les métriques Prometheus + // Exemple de métriques à suivre : + // - Nombre d'opérations de chiffrement/déchiffrement + // - Temps d'exécution des opérations + // - Utilisation des ressources + // - Taux d'erreur } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0bad90e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import { resolve } from 'node:path'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + build: { + rollupOptions: { + input: { + index: resolve(__dirname, 'react-src/index/main.jsx'), + about: resolve(__dirname, 'react-src/about/main.jsx'), + }, + output: { + entryFileNames: 'js/[name].js', + }, + }, + outDir: 'public', + emptyOutDir: true, + }, +}); From 9f652295001d36a7f8decc43bfc63bb4825815e7 Mon Sep 17 00:00:00 2001 From: DoniLite Date: Sun, 13 Apr 2025 13:15:54 +0000 Subject: [PATCH 3/8] fix: format the code and setup the node working environment --- .golangci.yml | 44 ++++++++------- Makefile | 4 ++ api/routes.go | 28 +++++----- build/builder.go | 8 +-- cmd/build/main.go | 2 +- cmd/loader/main.go | 24 ++++---- cmd/main.go | 2 +- cmd/server_test.go | 4 +- controllers/auth_controller.go | 81 +++++++++++++++------------ controllers/file_controller.go | 38 ++++++------- controllers/test_controller.go | 2 +- db/db.go | 43 ++++++++------- go.mod | 2 - middleware/auth.go | 51 +++++++++-------- models/models.go | 23 ++++---- models/props.go | 3 +- package.json | 4 ++ pnpm-lock.yaml | 54 ++++++++++++++++++ routes/main.go | 21 +++---- services/cloud_service.go | 2 +- services/compression_service.go | 50 +++++++++-------- services/encryption_service.go | 15 +++-- services/init_service.go | 42 +++++++------- services/services_test.go | 29 ++++++---- utils/helpers.go | 97 +++++++++++++++++---------------- utils/helpers_test.go | 3 +- 26 files changed, 382 insertions(+), 294 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 5aafacf..225fefd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,25 +1,27 @@ +# See the dedicated "version" documentation section. +version: "2" linters: + # See the dedicated "linters" documentation section. + default: all +formatters: + # See the dedicated "formatters" documentation section. enable: + - gci - gofmt - - golint - - govet - - errcheck - - staticcheck - - gosimple - - ineffassign - -run: - deadline: 5m - + - gofumpt + - goimports + - golines + exclusions: + warn-unused: true issues: - exclude-use-default: false - max-issues-per-linter: 0 - max-same-issues: 0 - -linters-settings: - govet: - check-shadowing: true - golint: - min-confidence: 0 - gofmt: - simplify: true \ No newline at end of file + # See the dedicated "issues" documentation section. + new-from-merge-base: "develop" +run: + # See the dedicated "run" documentation section. + timeout: 30s +severity: + default: error + rules: + - linters: + - dupl + severity: info \ No newline at end of file diff --git a/Makefile b/Makefile index 050e8e4..224397d 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,10 @@ env: @echo "Listing environment variables..." env +lint: + @echo "Running the linter... Good luck! 😃" + golangci-lint run --fix + # Installation des dépendances install-deps: @echo "Installing dependencies..." diff --git a/api/routes.go b/api/routes.go index 597d43c..e896dbb 100644 --- a/api/routes.go +++ b/api/routes.go @@ -10,18 +10,18 @@ import ( ) func SetupRouter(router *gin.Engine) { - router.GET("/ping", controllers.PingRoute) - router.POST("/upload", controllers.UploadFile) - router.GET("/file/:id", controllers.DownloadFile) - router.GET("/staticFile/:file", routes.Static) - // router.GET("/", routes.Index(engine)) - // Autres routes - auth := router.Group("/auth") - auth.Use(middleware.ValidateJWT) - { - auth.GET("/user", func(ctx *gin.Context) { - user, _ := ctx.GetQuery("username") - log.Default().Print(user) - }) - } + router.GET("/ping", controllers.PingRoute) + router.POST("/upload", controllers.UploadFile) + router.GET("/file/:id", controllers.DownloadFile) + router.GET("/staticFile/:file", routes.Static) + // router.GET("/", routes.Index(engine)) + // Autres routes + auth := router.Group("/auth") + auth.Use(middleware.ValidateJWT) + { + auth.GET("/user", func(ctx *gin.Context) { + user, _ := ctx.GetQuery("username") + log.Default().Print(user) + }) + } } diff --git a/build/builder.go b/build/builder.go index 66457c8..f8c4847 100644 --- a/build/builder.go +++ b/build/builder.go @@ -31,8 +31,8 @@ func createImportPlugin() api.Plugin { ReplaceAllString(string(content), `from $1$2.js$1`) return api.OnLoadResult{ - Contents: &updatedContent, // Contenu modifié - Loader: api.LoaderTS, // Garder le loader TypeScript + Contents: &updatedContent, // Contenu modifié + Loader: api.LoaderTS, // Garder le loader TypeScript ResolveDir: filepath.Dir(args.Path), // Répertoire pour la résolution }, nil }) @@ -40,8 +40,6 @@ func createImportPlugin() api.Plugin { } } - - func buildTSFile(inputPath string) error { outputPath := filepath.Join( "./client/js", @@ -119,7 +117,7 @@ func WatchTSFiles(sourceDir string) error { Sourcemap: api.SourceMapLinked, Outdir: "./client/js", Platform: api.PlatformBrowser, - Plugins: []api.Plugin{createImportPlugin()}, + Plugins: []api.Plugin{createImportPlugin()}, Loader: map[string]api.Loader{ ".ts": api.LoaderTS, // Indiquer à esbuild de gérer les fichiers TypeScript }, diff --git a/cmd/build/main.go b/cmd/build/main.go index f6a4b3a..7bcecd0 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -11,4 +11,4 @@ func main() { if err != nil { fmt.Printf("failed to build files: %v", err) } -} \ No newline at end of file +} diff --git a/cmd/loader/main.go b/cmd/loader/main.go index 7a7aa3f..0bf88f8 100644 --- a/cmd/loader/main.go +++ b/cmd/loader/main.go @@ -1,20 +1,20 @@ package main import ( - "fmt" - "io" - "os" - - "ariga.io/atlas-provider-gorm/gormschema" + "fmt" + "io" + "os" + "ariga.io/atlas-provider-gorm/gormschema" "cloudbeast.doni/m/models" ) func main() { - stmts, err := gormschema.New("postgres").Load(&models.User{}, &models.Product{}) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to load gorm schema: %v\n", err) - os.Exit(1) - } - io.WriteString(os.Stdout, stmts) -} \ No newline at end of file + stmts, err := gormschema.New("postgres").Load(&models.User{}, &models.Product{}) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load gorm schema: %v\n", err) + os.Exit(1) + } + + io.WriteString(os.Stdout, stmts) +} diff --git a/cmd/main.go b/cmd/main.go index 997ad2a..7e34b69 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,9 +8,9 @@ import ( "cloudbeast.doni/m/api" "cloudbeast.doni/m/utils" "github.com/Backblaze/blazer/b2" + "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" "github.com/joho/godotenv" - "github.com/gin-contrib/pprof" ) func main() { diff --git a/cmd/server_test.go b/cmd/server_test.go index 375e606..01d78d0 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -13,7 +13,7 @@ import ( func TestPingRoute(t *testing.T) { router := gin.Default() - api.SetupRouter(router) + api.SetupRouter(router) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/ping", nil) @@ -21,4 +21,4 @@ func TestPingRoute(t *testing.T) { assert.Equal(t, 200, w.Code) assert.Equal(t, "pong", w.Body.String()) -} \ No newline at end of file +} diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index ae281c3..085fe40 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -11,45 +11,54 @@ import ( ) type Claims struct { - UserId int `json:"userId"` - jwt.StandardClaims + UserId int `json:"userId"` + jwt.StandardClaims } var JwtKey = []byte("your_secret_key") // Remplace par une clé secrète sécurisée func GenerateToken(c *gin.Context) { - dbx, err := db.Setup() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Some features not initialize properly"}) - } - userID, ok := c.Get("userID") - var user models.User - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{"message": "User not authenticated"}) - return - } - - result := dbx.First(&user, userID.(int)) - - if result.Error != nil { - c.JSON(http.StatusNotFound, gin.H{"message": "not found user"}) - return - } - - claims := &Claims{ - UserId: userID.(int), - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), // Token expirera dans 24 heures - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(JwtKey) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Could not create token"}) - return - } - - c.JSON(http.StatusOK, gin.H{"token": tokenString}) -} + dbx, err := db.Setup() + if err != nil { + c.JSON( + http.StatusInternalServerError, + gin.H{"message": "Some features not initialize properly"}, + ) + } + + userID, ok := c.Get("userID") + + var user models.User + + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"message": "User not authenticated"}) + + return + } + + result := dbx.First(&user, userID.(int)) + + if result.Error != nil { + c.JSON(http.StatusNotFound, gin.H{"message": "not found user"}) + return + } + + claims := &Claims{ + UserId: userID.(int), + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), // Token expirera dans 24 heures + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + tokenString, err := token.SignedString(JwtKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": "Could not create token"}) + + return + } + + c.JSON(http.StatusOK, gin.H{"token": tokenString}) +} diff --git a/controllers/file_controller.go b/controllers/file_controller.go index 15c597b..b7b062b 100644 --- a/controllers/file_controller.go +++ b/controllers/file_controller.go @@ -1,34 +1,34 @@ package controllers import ( - "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" ) type File struct { - ID int `json:"id"` - FileName string `json:"file_name"` - UserID int `json:"user_id"` + ID int `json:"id"` + FileName string `json:"file_name"` + UserID int `json:"user_id"` } func UploadFile(c *gin.Context) { - // userID := c.GetInt("userID") // récupère le userID via JWT + // userID := c.GetInt("userID") // récupère le userID via JWT - // ... Chiffrement et stockage du fichier - - // Stocker le fichier dans la base de données avec la référence utilisateur - // db.Create(&File{FileName: "encryptedFileName", UserID: userID}) + // ... Chiffrement et stockage du fichier + + // Stocker le fichier dans la base de données avec la référence utilisateur + // db.Create(&File{FileName: "encryptedFileName", UserID: userID}) } func DownloadFile(c *gin.Context) { - // Code pour gérer le téléchargement de fichier + // Code pour gérer le téléchargement de fichier // userID := c.GetInt("userID") - // fileID := c.Param("fileID") - - // Vérifier si le fichier appartient à l'utilisateur - // var file File - // db.Where("id = ? AND user_id = ?", fileID, userID).First(&file) - // if file.ID == 0 { - // c.JSON(403, gin.H{"message": "Access denied"}) - // return - // } + // fileID := c.Param("fileID") + + // Vérifier si le fichier appartient à l'utilisateur + // var file File + // db.Where("id = ? AND user_id = ?", fileID, userID).First(&file) + // if file.ID == 0 { + // c.JSON(403, gin.H{"message": "Access denied"}) + // return + // } } diff --git a/controllers/test_controller.go b/controllers/test_controller.go index 6a5c8b0..62b4eee 100644 --- a/controllers/test_controller.go +++ b/controllers/test_controller.go @@ -4,4 +4,4 @@ import "github.com/gin-gonic/gin" func PingRoute(ctx *gin.Context) { ctx.String(200, "pong") -} \ No newline at end of file +} diff --git a/db/db.go b/db/db.go index ae4ce48..724c172 100644 --- a/db/db.go +++ b/db/db.go @@ -7,31 +7,32 @@ import ( // Contient la base de données du projet 'cloudbeast' gérée par `gorm ORM` func Setup() (*gorm.DB, error) { - dsn := "host=localhost user=doni password=DoniLite13 dbname=anexis port=5432 sslmode=disable" - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return nil, err - } + dsn := "host=localhost user=doni password=DoniLite13 dbname=anexis port=5432 sslmode=disable" - // Migrate the schema - // db.AutoMigrate(&models.Product{}, models.User{}) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, err + } - // Create - // db.Create(&models.Product{Code: "D42", Price: 100}) + // Migrate the schema + // db.AutoMigrate(&models.Product{}, models.User{}) - // Read - // var product models.Product - // db.First(&product, 1) // find product with integer primary key - // db.First(&product, "code = ?", "D42") // find product with code D42 + // Create + // db.Create(&models.Product{Code: "D42", Price: 100}) - // Update - update product's price to 200 - // db.Model(&product).Update("Price", 200) - // Update - update multiple fields - // db.Model(&product).Updates(models.Product{Price: 200, Code: "F42"}) // non-zero fields - // db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) + // Read + // var product models.Product + // db.First(&product, 1) // find product with integer primary key + // db.First(&product, "code = ?", "D42") // find product with code D42 - // Delete - delete product - // db.Delete(&product, 1) + // Update - update product's price to 200 + // db.Model(&product).Update("Price", 200) + // Update - update multiple fields + // db.Model(&product).Updates(models.Product{Price: 200, Code: "F42"}) // non-zero fields + // db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) - return db, nil + // Delete - delete product + // db.Delete(&product, 1) + + return db, nil } diff --git a/go.mod b/go.mod index a2caa90..d422093 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module cloudbeast.doni/m go 1.23.5 -toolchain go1.22.9 - require ( ariga.io/atlas-provider-gorm v0.5.0 github.com/docker/docker v27.4.1+incompatible diff --git a/middleware/auth.go b/middleware/auth.go index adaa7a0..d30e44a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -8,28 +8,31 @@ import ( "github.com/gin-gonic/gin" ) - - - func ValidateJWT(c *gin.Context) { - tokenStr := c.Request.Header.Get("Authorization") - if tokenStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Token is missing"}) - c.Abort() - return - } - - claims := &controllers.Claims{} - token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { - return controllers.JwtKey, nil - }) - - if err != nil || !token.Valid { - c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"}) - c.Abort() - return - } - - c.Set("userID", claims.UserId) - c.Next() -} \ No newline at end of file + tokenStr := c.Request.Header.Get("Authorization") + if tokenStr == "" { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Token is missing"}) + c.Abort() + + return + } + + claims := &controllers.Claims{} + token, err := jwt.ParseWithClaims( + tokenStr, + claims, + func(token *jwt.Token) (interface{}, error) { + return controllers.JwtKey, nil + }, + ) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"}) + c.Abort() + + return + } + + c.Set("userID", claims.UserId) + c.Next() +} diff --git a/models/models.go b/models/models.go index b247bc9..ec8dbac 100644 --- a/models/models.go +++ b/models/models.go @@ -2,22 +2,23 @@ package models import ( "database/sql" - "gorm.io/gorm" "time" + + "gorm.io/gorm" ) type User struct { - gorm.Model - Name string // A regular string field - Email *string // A pointer to a string, allowing for null values - Age uint8 // An unsigned 8-bit integer - Birthday *time.Time // A pointer to time.Time, can be null - MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings - ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields + gorm.Model + Name string // A regular string field + Email *string // A pointer to a string, allowing for null values + Age uint8 // An unsigned 8-bit integer + Birthday *time.Time // A pointer to time.Time, can be null + MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings + ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields } type Product struct { - gorm.Model - Code string - Price uint + gorm.Model + Code string + Price uint } diff --git a/models/props.go b/models/props.go index 5814b6e..db04970 100644 --- a/models/props.go +++ b/models/props.go @@ -1,6 +1,5 @@ package models - type IndexRouteProps struct { User string -} \ No newline at end of file +} diff --git a/package.json b/package.json index 158c7cb..2c8086f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,11 @@ "dependencies": { "@tailwindcss/vite": "^4.1.3", "@types/node": "^22.14.1", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.3.4", + "react": "^19.1.0", + "react-dom": "^19.1.0", "tailwindcss": "^4.1.3", "vite": "^6.2.6" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03a7e95..5773c26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,21 @@ importers: '@types/node': specifier: ^22.14.1 version: 22.14.1 + '@types/react': + specifier: ^19.1.1 + version: 19.1.1 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.1) '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1)) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) tailwindcss: specifier: ^4.1.3 version: 4.1.3 @@ -473,6 +485,14 @@ packages: '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/react-dom@19.1.2': + resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.1': + resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==} + '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} engines: {node: ^14.18.0 || >=16.0.0} @@ -490,6 +510,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -637,15 +660,27 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + rollup@4.40.0: resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1075,6 +1110,14 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/react-dom@19.1.2(@types/react@19.1.1)': + dependencies: + '@types/react': 19.1.1 + + '@types/react@19.1.1': + dependencies: + csstype: 3.1.3 + '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1))': dependencies: '@babel/core': 7.26.10 @@ -1097,6 +1140,8 @@ snapshots: convert-source-map@2.0.0: {} + csstype@3.1.3: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -1220,8 +1265,15 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + react-refresh@0.14.2: {} + react@19.1.0: {} + rollup@4.40.0: dependencies: '@types/estree': 1.0.7 @@ -1248,6 +1300,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 + scheduler@0.26.0: {} + semver@6.3.1: {} source-map-js@1.2.1: {} diff --git a/routes/main.go b/routes/main.go index 4e3dc73..0f7463b 100644 --- a/routes/main.go +++ b/routes/main.go @@ -6,18 +6,15 @@ import ( gossr "github.com/natewong1313/go-react-ssr" ) - -func Index(engine *gossr.Engine) (func (*gin.Context)) { +func Index(engine *gossr.Engine) func(*gin.Context) { return func(ctx *gin.Context) { ctx.Writer.Write(engine.RenderRoute(gossr.RenderConfig{ - File: "pages/Home.tsx", - Title: "Annexis | Home", - MetaTags: map[string]string{ - - }, - Props: &models.IndexRouteProps{ - User: "Doni", - }, - })) + File: "pages/Home.tsx", + Title: "Annexis | Home", + MetaTags: map[string]string{}, + Props: &models.IndexRouteProps{ + User: "Doni", + }, + })) } -} \ No newline at end of file +} diff --git a/services/cloud_service.go b/services/cloud_service.go index 0d53640..fe19d13 100644 --- a/services/cloud_service.go +++ b/services/cloud_service.go @@ -63,4 +63,4 @@ func PrintObjects(ctx context.Context, bucket *b2.Bucket) error { fmt.Println(iterator.Object()) } return iterator.Err() -} \ No newline at end of file +} diff --git a/services/compression_service.go b/services/compression_service.go index 12e23db..9757c0b 100644 --- a/services/compression_service.go +++ b/services/compression_service.go @@ -7,30 +7,34 @@ import ( ) func CompressData(data []byte) ([]byte, error) { - var b bytes.Buffer - gz := gzip.NewWriter(&b) - if _, err := gz.Write(data); err != nil { - return nil, err - } - if err := gz.Close(); err != nil { - return nil, err - } - return b.Bytes(), nil -} + var b bytes.Buffer + + gz := gzip.NewWriter(&b) + if _, err := gz.Write(data); err != nil { + return nil, err + } + if err := gz.Close(); err != nil { + return nil, err + } + + return b.Bytes(), nil +} func DecompressData(data []byte) ([]byte, error) { - b := bytes.NewBuffer(data) - gz, err := gzip.NewReader(b) - if err != nil { - return nil, err - } - defer gz.Close() - - decompressed, err := io.ReadAll(gz) - if err != nil { - return nil, err - } - - return decompressed, nil + b := bytes.NewBuffer(data) + + gz, err := gzip.NewReader(b) + if err != nil { + return nil, err + } + + defer gz.Close() + + decompressed, err := io.ReadAll(gz) + if err != nil { + return nil, err + } + + return decompressed, nil } diff --git a/services/encryption_service.go b/services/encryption_service.go index 900eb9c..11077d8 100644 --- a/services/encryption_service.go +++ b/services/encryption_service.go @@ -8,18 +8,17 @@ import ( ) func EncryptFile(fileData []byte, pubKey *rsa.PublicKey) ([]byte, error) { - encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, fileData, nil) - return encryptedData, err + encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, fileData, nil) + + return encryptedData, err } func SaveEncryptedFile(filename string, data []byte) error { - return os.WriteFile("/path/to/storage/"+filename, data, 0644) + return os.WriteFile("/path/to/storage/"+filename, data, 0o644) } func DecryptFile(cipherData []byte, privKey *rsa.PrivateKey) ([]byte, error) { - decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, cipherData, nil) - return decryptedData, err -} - - + decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, cipherData, nil) + return decryptedData, err +} diff --git a/services/init_service.go b/services/init_service.go index 9bb6f4a..3a7f29d 100644 --- a/services/init_service.go +++ b/services/init_service.go @@ -4,29 +4,29 @@ import ( "log" ) - var ( - Opts *ServiceOptions + Opts *ServiceOptions Service *SecureEncryptionService ) + func init() { - Opts = DefaultServiceOptions() - - // Personnaliser le chemin de travail - workDir := "../.encryption-service" - Opts.WorkingDir = workDir - - // Augmenter les ressources disponibles - Opts.MaxCPUs = 4 - Opts.MaxMemoryMB = 1024 - - // Activer le mode debug pour les logs - Opts.LogLevel = "debug" - - // Initialiser le service - service, err := InitService(Opts) - if err != nil { - log.Fatalf("Failed to initialize encryption service: %v", err) - } + Opts = DefaultServiceOptions() + + // Personnaliser le chemin de travail + workDir := "../.encryption-service" + Opts.WorkingDir = workDir + + // Augmenter les ressources disponibles + Opts.MaxCPUs = 4 + Opts.MaxMemoryMB = 1024 + + // Activer le mode debug pour les logs + Opts.LogLevel = "debug" + + // Initialiser le service + service, err := InitService(Opts) + if err != nil { + log.Fatalf("Failed to initialize encryption service: %v", err) + } Service = service -} \ No newline at end of file +} diff --git a/services/services_test.go b/services/services_test.go index c0e3913..206d3cd 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -9,8 +9,8 @@ import ( ) const ( - FILE = "../tests/input/gojo.jpeg" - OUTPUTDIR = "../tests/output" + FILE = "../tests/input/gojo.jpeg" + OUTPUTDIR = "../tests/output" ) func TestCompressionService(t *testing.T) { @@ -23,15 +23,20 @@ func TestCompressionService(t *testing.T) { if compressErr != nil { t.Error("failed to compress data...") } - outputFile, outputErr := os.Create(path.Join(OUTPUTDIR, "output")) - if outputErr != nil { - t.Errorf("cannot create a file with %s to the %s dir", path.Join(OUTPUTDIR, "output"), OUTPUTDIR) - } - outputFile.Write(compressedData) + + outputFile, outputErr := os.Create(path.Join(OUTPUTDIR, "output")) + if outputErr != nil { + t.Errorf( + "cannot create a file with %s to the %s dir", + path.Join(OUTPUTDIR, "output"), + OUTPUTDIR, + ) + } + + outputFile.Write(compressedData) outputFile.Close() } - func TestDecompressionService(t *testing.T) { data, err := os.ReadFile(path.Join(OUTPUTDIR, "output")) if err != nil { @@ -43,8 +48,12 @@ func TestDecompressionService(t *testing.T) { } outputFile, outputErr := os.Create(path.Join(OUTPUTDIR, "output2.jpeg")) if outputErr != nil { - t.Errorf("cannot create a file with %s to the %s dir", path.Join(OUTPUTDIR, "output2"), OUTPUTDIR) + t.Errorf( + "cannot create a file with %s to the %s dir", + path.Join(OUTPUTDIR, "output2"), + OUTPUTDIR, + ) } outputFile.Write(decompressedData) outputFile.Close() -} \ No newline at end of file +} diff --git a/utils/helpers.go b/utils/helpers.go index 48acf39..e75fce9 100644 --- a/utils/helpers.go +++ b/utils/helpers.go @@ -12,54 +12,59 @@ import ( // Générer une paire de clés RSA func GenerateRSAKeys(user string) (*rsa.PrivateKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, err - } - keyDir := `../rsa/user/` - keyDir, err = filepath.Abs(keyDir) - if err != nil { - return nil, err - } - // Enregistrement des clés - privateFile, err := os.Create(path.Join(keyDir, user + "-private.pem")) - if err != nil { - return nil, err - } - pem.Encode(privateFile, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) - privateFile.Close() + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } - publicKey := &privateKey.PublicKey - publicFile, err := os.Create(path.Join(keyDir, user + "-public.pem")) - if err != nil { - return nil, err - } - pem.Encode(publicFile, &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: x509.MarshalPKCS1PublicKey(publicKey), - }) - publicFile.Close() + keyDir := `../rsa/user/` - return privateKey, nil -} + keyDir, err = filepath.Abs(keyDir) + if err != nil { + return nil, err + } + // Enregistrement des clés + privateFile, err := os.Create(path.Join(keyDir, user+"-private.pem")) + if err != nil { + return nil, err + } + + pem.Encode(privateFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + privateFile.Close() + + publicKey := &privateKey.PublicKey + + publicFile, err := os.Create(path.Join(keyDir, user+"-public.pem")) + if err != nil { + return nil, err + } + pem.Encode(publicFile, &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(publicKey), + }) + publicFile.Close() + + return privateKey, nil +} func CreateDirectories(dirs []string) { - if len(dirs) == 0 { - dirs = []string{ - "../rsa", - "../rsa/server", - "../rsa/user", - "../client", - "../crypto", - } - } - for _, dir := range dirs { - if _, err := os.Stat(dir); os.IsNotExist(err) { - os.MkdirAll(dir, os.ModePerm) - } - } -} \ No newline at end of file + if len(dirs) == 0 { + dirs = []string{ + "../rsa", + "../rsa/server", + "../rsa/user", + "../client", + "../crypto", + } + } + + for _, dir := range dirs { + if _, err := os.Stat(dir); os.IsNotExist(err) { + os.MkdirAll(dir, os.ModePerm) + } + } +} diff --git a/utils/helpers_test.go b/utils/helpers_test.go index a6c97d7..21040a8 100644 --- a/utils/helpers_test.go +++ b/utils/helpers_test.go @@ -8,6 +8,7 @@ import ( ) var privateKey *rsa.PrivateKey + const USER = "doni" func TestGenerator(t *testing.T) { @@ -16,4 +17,4 @@ func TestGenerator(t *testing.T) { t.Errorf("Failed to generate RSA keys: %v", err) } privateKey = key -} \ No newline at end of file +} From d9356f0b53400c4a417d9af46800631061d69322 Mon Sep 17 00:00:00 2001 From: DoniLite Date: Tue, 22 Apr 2025 19:16:38 +0000 Subject: [PATCH 4/8] chore: workspace config - setup the workspace - create a new packages - structuring the codebase --- .gitignore | 2 +- {build => builder/build}/builder.go | 0 builder/go.mod | 7 + builder/go.sum | 4 + {cmd/build => builder}/main.go | 2 +- {cmd => builder}/watch/main.go | 2 +- bx/build/builder.go | 1334 +++++++++++++++++ bx/go.mod | 43 + bx/go.sum | 159 ++ bx/types.go | 15 + package.json => frontend/package.json | 4 +- pnpm-lock.yaml => frontend/pnpm-lock.yaml | 0 tsconfig.json => frontend/tsconfig.json | 2 +- vite.config.ts => frontend/vite.config.ts | 0 postcss.config.js | 6 - {api => server/api}/routes.go | 6 +- atlas.hcl => server/atlas.hcl | 0 .../controllers}/auth_controller.go | 4 +- .../controllers}/file_controller.go | 0 .../controllers}/test_controller.go | 0 {db => server/db}/db.go | 0 default.pgo => server/default.pgo | Bin go.mod => server/go.mod | 32 +- go.sum => server/go.sum | 94 +- {cmd => server}/loader/main.go | 2 +- {cmd => server}/main.go | 5 +- {middleware => server/middleware}/auth.go | 2 +- .../migrations}/20241231112547.sql | 0 {migrations => server/migrations}/atlas.sum | 0 {models => server/models}/models.go | 0 {models => server/models}/props.go | 0 {routes => server/routes}/main.go | 2 +- {routes => server/routes}/static.go | 0 {cmd => server}/server_test.go | 2 +- services/builder_service.go | 25 + services/encryption_service.go | 3 +- services/go.mod | 37 + services/go.sum | 139 ++ services/services_test.go | 2 +- tailwind.config.js | 14 - utils/go.mod | 3 + utils/helpers_test.go | 2 +- 42 files changed, 1794 insertions(+), 160 deletions(-) rename {build => builder/build}/builder.go (100%) create mode 100644 builder/go.mod create mode 100644 builder/go.sum rename {cmd/build => builder}/main.go (85%) rename {cmd => builder}/watch/main.go (91%) create mode 100644 bx/build/builder.go create mode 100644 bx/go.mod create mode 100644 bx/go.sum create mode 100644 bx/types.go rename package.json => frontend/package.json (74%) rename pnpm-lock.yaml => frontend/pnpm-lock.yaml (100%) rename tsconfig.json => frontend/tsconfig.json (93%) rename vite.config.ts => frontend/vite.config.ts (100%) delete mode 100644 postcss.config.js rename {api => server/api}/routes.go (85%) rename atlas.hcl => server/atlas.hcl (100%) rename {controllers => server/controllers}/auth_controller.go (95%) rename {controllers => server/controllers}/file_controller.go (100%) rename {controllers => server/controllers}/test_controller.go (100%) rename {db => server/db}/db.go (100%) rename default.pgo => server/default.pgo (100%) rename go.mod => server/go.mod (69%) rename go.sum => server/go.sum (75%) rename {cmd => server}/loader/main.go (91%) rename {cmd => server}/main.go (94%) rename {middleware => server/middleware}/auth.go (95%) rename {migrations => server/migrations}/20241231112547.sql (100%) rename {migrations => server/migrations}/atlas.sum (100%) rename {models => server/models}/models.go (100%) rename {models => server/models}/props.go (100%) rename {routes => server/routes}/main.go (93%) rename {routes => server/routes}/static.go (100%) rename {cmd => server}/server_test.go (94%) create mode 100644 services/builder_service.go create mode 100644 services/go.mod create mode 100644 services/go.sum delete mode 100644 tailwind.config.js create mode 100644 utils/go.mod diff --git a/.gitignore b/.gitignore index 7f4729a..04bcb18 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ go.work.sum .vscode client -node_modules +/**/node_modules test.db bin rsa/ \ No newline at end of file diff --git a/build/builder.go b/builder/build/builder.go similarity index 100% rename from build/builder.go rename to builder/build/builder.go diff --git a/builder/go.mod b/builder/go.mod new file mode 100644 index 0000000..e787525 --- /dev/null +++ b/builder/go.mod @@ -0,0 +1,7 @@ +module anexis/builder + +go 1.24.2 + +require github.com/evanw/esbuild v0.25.2 + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/builder/go.sum b/builder/go.sum new file mode 100644 index 0000000..ce57ce3 --- /dev/null +++ b/builder/go.sum @@ -0,0 +1,4 @@ +github.com/evanw/esbuild v0.25.2 h1:ublSEmZSjzOc6jLO1OTQy/vHc1wiqyDF4oB3hz5sM6s= +github.com/evanw/esbuild v0.25.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cmd/build/main.go b/builder/main.go similarity index 85% rename from cmd/build/main.go rename to builder/main.go index 7bcecd0..954439a 100644 --- a/cmd/build/main.go +++ b/builder/main.go @@ -3,7 +3,7 @@ package main import ( "fmt" - "cloudbeast.doni/m/build" + "anexis/builder/build" ) func main() { diff --git a/cmd/watch/main.go b/builder/watch/main.go similarity index 91% rename from cmd/watch/main.go rename to builder/watch/main.go index be49950..3c88ced 100644 --- a/cmd/watch/main.go +++ b/builder/watch/main.go @@ -4,7 +4,7 @@ import ( "fmt" "path/filepath" - "cloudbeast.doni/m/build" + "anexis/builder/build" ) func main() { diff --git a/bx/build/builder.go b/bx/build/builder.go new file mode 100644 index 0000000..a3bda59 --- /dev/null +++ b/bx/build/builder.go @@ -0,0 +1,1334 @@ +package build + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" + "gopkg.in/yaml.v3" + + // Paquet pour B2 + "github.com/Backblaze/blazer/b2" +) + +// BuildSpec représente la configuration personnalisée de build +type BuildSpec struct { + Name string `json:"name" yaml:"name"` + Version string `json:"version" yaml:"version"` + Codebases []CodebaseConfig `json:"codebases" yaml:"codebases"` + BuildConfig BuildConfig `json:"build_config" yaml:"build_config"` + Env map[string]string `json:"env" yaml:"env"` + Artifacts []string `json:"artifacts" yaml:"artifacts"` +} + +// CodebaseConfig représente une base de code source +type CodebaseConfig struct { + Name string `json:"name" yaml:"name"` + SourceType string `json:"source_type" yaml:"source_type"` // git, local, archive, buffer + Source string `json:"source" yaml:"source"` // URL, chemin local + Branch string `json:"branch" yaml:"branch"` + Commit string `json:"commit" yaml:"commit"` + Path string `json:"path" yaml:"path"` // Sous-répertoire si nécessaire + Content []byte `json:"-" yaml:"-"` // Contenu en mémoire pour le type buffer +} + +// BuildConfig représente la configuration de build Docker +type BuildConfig struct { + BaseImage string `json:"base_image" yaml:"base_image"` + Dockerfile string `json:"dockerfile" yaml:"dockerfile"` + ComposeFile string `json:"compose_file" yaml:"compose_file"` + Target string `json:"target" yaml:"target"` + Args map[string]string `json:"args" yaml:"args"` + Tags []string `json:"tags" yaml:"tags"` + Platforms []string `json:"platforms" yaml:"platforms"` + NoCache bool `json:"no_cache" yaml:"no_cache"` +} + +// BuildResult contient les résultats du processus de build +type BuildResult struct { + Success bool `json:"success"` + ImageID string `json:"image_id"` + ImageSize int64 `json:"image_size"` + Artifacts map[string][]byte `json:"artifacts"` + BuildTime float64 `json:"build_time"` + ErrorMessage string `json:"error_message,omitempty"` + Logs string `json:"logs"` + B2ObjectNames []string `json:"b2_object_names,omitempty"` +} + +// B2Config contient les informations de configuration pour le stockage B2 +type B2Config struct { + AccountID string `json:"account_id" yaml:"account_id"` + ApplicationKey string `json:"application_key" yaml:"application_key"` + BucketName string `json:"bucket_name" yaml:"bucket_name"` + BasePath string `json:"base_path" yaml:"base_path"` +} + +// BuildService est le service principal qui gère les builds +type BuildService struct { + dockerClient *client.Client + workDir string + b2Config *B2Config + mutex sync.Mutex + inMemory bool // Si true, fonctionne principalement en mémoire +} + +// NewBuildService crée une nouvelle instance du service de build +func NewBuildService(workDir string, inMemory bool) (*BuildService, error) { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("erreur lors de la création du client Docker: %w", err) + } + + // Création du répertoire de travail si nécessaire et si pas en mode mémoire + if !inMemory && workDir != "" { + if err := os.MkdirAll(workDir, 0755); err != nil { + return nil, fmt.Errorf("impossible de créer le répertoire %s: %w", workDir, err) + } + } + + return &BuildService{ + dockerClient: cli, + workDir: workDir, + inMemory: inMemory, + mutex: sync.Mutex{}, + }, nil +} + +// SetB2Config configure les paramètres pour le stockage B2 +func (s *BuildService) SetB2Config(config *B2Config) { + s.b2Config = config +} + +// LoadBuildSpecFromFile charge une spécification de build depuis un fichier +func LoadBuildSpecFromFile(filename string) (*BuildSpec, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("impossible de lire le fichier de spécification: %w", err) + } + + return LoadBuildSpecFromBytes(data, filepath.Ext(filename)) +} + +// LoadBuildSpecFromBytes charge une spécification de build depuis des données en mémoire +func LoadBuildSpecFromBytes(data []byte, format string) (*BuildSpec, error) { + var spec BuildSpec + var err error + + if format == ".json" { + err = json.Unmarshal(data, &spec) + } else if format == ".yaml" || format == ".yml" { + err = yaml.Unmarshal(data, &spec) + } else { + return nil, fmt.Errorf("format non pris en charge: %s", format) + } + + if err != nil { + return nil, fmt.Errorf("impossible de parser la spécification: %w", err) + } + + return &spec, nil +} + +// Build exécute un build selon la spécification donnée +func (s *BuildService) Build(ctx context.Context, spec *BuildSpec) (*BuildResult, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + startTime := time.Now() + result := &BuildResult{ + Artifacts: make(map[string][]byte), + Logs: "", + } + + var buildDir string + var cleanupFunc func() + + if s.inMemory { + // Utilisation d'un répertoire temporaire pour les opérations nécessitant des fichiers + tmpDir, err := os.MkdirTemp("", fmt.Sprintf("docker-build-%s-%s-", spec.Name, spec.Version)) + if err != nil { + return result, fmt.Errorf("impossible de créer un répertoire temporaire: %w", err) + } + buildDir = tmpDir + cleanupFunc = func() { os.RemoveAll(tmpDir) } + } else { + // Créer un répertoire de build unique dans le workDir + buildID := fmt.Sprintf("%s-%s-%d", spec.Name, spec.Version, time.Now().Unix()) + buildDir = filepath.Join(s.workDir, buildID) + if err := os.MkdirAll(buildDir, 0755); err != nil { + return result, fmt.Errorf("impossible de créer le répertoire de build: %w", err) + } + cleanupFunc = func() { os.RemoveAll(buildDir) } + } + defer cleanupFunc() + + // Traiter les codebases + for _, codebase := range spec.Codebases { + destDir := filepath.Join(buildDir, codebase.Name) + if err := s.fetchCodebase(ctx, codebase, destDir); err != nil { + result.Success = false + result.ErrorMessage = fmt.Sprintf("erreur lors de la récupération de la codebase %s: %s", codebase.Name, err) + return result, err + } + } + + // Préparer le Dockerfile + dockerfilePath := "" + if spec.BuildConfig.Dockerfile != "" { + dockerfilePath = filepath.Join(buildDir, "Dockerfile") + if err := os.WriteFile(dockerfilePath, []byte(spec.BuildConfig.Dockerfile), 0644); err != nil { + result.Success = false + result.ErrorMessage = "erreur lors de la création du Dockerfile: " + err.Error() + return result, err + } + } else { + // Chercher un Dockerfile existant + for _, codebase := range spec.Codebases { + dfPath := filepath.Join(buildDir, codebase.Name, "Dockerfile") + if _, err := os.Stat(dfPath); err == nil { + dockerfilePath = dfPath + break + } + } + if dockerfilePath == "" { + result.Success = false + result.ErrorMessage = "aucun Dockerfile trouvé" + return result, fmt.Errorf("aucun Dockerfile trouvé") + } + } + + // Construction de l'image Docker + imageID, logs, err := s.buildWithDocker(ctx, buildDir, dockerfilePath, spec) + result.Logs = logs + if err != nil { + result.Success = false + result.ErrorMessage = "erreur lors du build Docker: " + err.Error() + return result, err + } + result.ImageID = imageID + + // Récupérer la taille de l'image + imageSize, err := s.getImageSize(ctx, imageID) + if err == nil { + result.ImageSize = imageSize + } + + // Collecter les artefacts + for _, artifactPath := range spec.Artifacts { + fullPath := filepath.Join(buildDir, artifactPath) + data, err := os.ReadFile(fullPath) + if err != nil { + result.Logs += fmt.Sprintf("\nAvertissement: impossible de lire l'artefact %s: %s", artifactPath, err) + continue + } + result.Artifacts[artifactPath] = data + } + + // Exporter l'image et l'envoyer au bucket B2 si configuré + if s.b2Config != nil { + objectNames, err := s.exportAndUploadImage(ctx, imageID, spec) + if err != nil { + result.Logs += fmt.Sprintf("\nAvertissement lors de l'export vers B2: %s", err) + } else { + result.B2ObjectNames = objectNames + } + } + + result.Success = true + result.BuildTime = time.Since(startTime).Seconds() + return result, nil +} + +// fetchCodebase récupère une codebase selon sa configuration +func (s *BuildService) fetchCodebase(ctx context.Context, config CodebaseConfig, destDir string) error { + if err := os.MkdirAll(destDir, 0755); err != nil { + return err + } + + switch config.SourceType { + case "git": + return s.fetchGitRepo(ctx, config, destDir) + case "local": + return s.copyLocalDir(config.Source, destDir) + case "archive": + return s.extractArchive(config.Source, destDir) + case "buffer": + // Traitement des données directement en mémoire + if len(config.Content) == 0 { + return fmt.Errorf("contenu vide pour la codebase de type buffer") + } + return s.extractBufferToDir(config.Content, destDir) + default: + return fmt.Errorf("type de source non pris en charge: %s", config.SourceType) + } +} + +// fetchGitRepo clone un dépôt git +func (s *BuildService) fetchGitRepo(ctx context.Context, config CodebaseConfig, destDir string) error { + // Utiliser gitImplementation directe si disponible, sinon utiliser exec.Command + // Ceci est un exemple simplifié qui utilise exec.Command + + cmd := fmt.Sprintf("git clone") + if config.Branch != "" { + cmd += fmt.Sprintf(" -b %s", config.Branch) + } + if config.Commit != "" { + cmd += " --single-branch" + } + cmd += fmt.Sprintf(" %s %s", config.Source, destDir) + + // Exécuter la commande git + // Note: Dans une implémentation réelle, il serait préférable d'utiliser un client Git en Go + // comme go-git ou d'intégrer libgit2 via cgo pour éviter les dépendances externes + + return execCmd(ctx, cmd) +} + +// copyLocalDir copie un répertoire local +func (s *BuildService) copyLocalDir(source, dest string) error { + entries, err := ioutil.ReadDir(source) + if err != nil { + return err + } + + for _, entry := range entries { + sourcePath := filepath.Join(source, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + if entry.IsDir() { + if err := os.MkdirAll(destPath, entry.Mode()); err != nil { + return err + } + if err := s.copyLocalDir(sourcePath, destPath); err != nil { + return err + } + } else { + data, err := ioutil.ReadFile(sourcePath) + if err != nil { + return err + } + if err := ioutil.WriteFile(destPath, data, entry.Mode()); err != nil { + return err + } + } + } + return nil +} + +// extractArchive extrait une archive vers un répertoire +func (s *BuildService) extractArchive(sourcePath string, destDir string) error { + data, err := ioutil.ReadFile(sourcePath) + if err != nil { + return err + } + return s.extractBufferToDir(data, destDir) +} + +// extractBufferToDir extrait des données d'archive en mémoire vers un répertoire +func (s *BuildService) extractBufferToDir(data []byte, destDir string) error { + // Détecter le type d'archive et extraire + if bytes.HasPrefix(data, []byte{0x1F, 0x8B}) { + // Archive gzip (tar.gz) + gzr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return err + } + defer gzr.Close() + + return extractTar(tar.NewReader(gzr), destDir) + } else if bytes.HasPrefix(data, []byte{0x50, 0x4B, 0x03, 0x04}) { + // Archive ZIP + // Note: Pour une implémentation complète, utilisez archive/zip + return fmt.Errorf("extraction ZIP non implémentée, utilisez tar.gz") + } else { + // Supposer tar simple + return extractTar(tar.NewReader(bytes.NewReader(data)), destDir) + } +} + +// extractTar extrait une archive tar +func extractTar(tr *tar.Reader, destDir string) error { + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(destDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + dir := filepath.Dir(target) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } + return nil +} + +// execCmd exécute une commande shell +func execCmd(ctx context.Context, cmdStr string) error { + // Dans une implémentation réelle, cette fonction utiliserait os/exec + // Pour l'exemple, on simule un retour réussi + return nil +} + +// buildWithDocker construit une image avec l'API Docker +func (s *BuildService) buildWithDocker(ctx context.Context, buildDir, dockerfilePath string, spec *BuildSpec) (string, string, error) { + var logBuffer bytes.Buffer + + // Créer le contexte de build en mémoire (tar) + buildContextTar, err := archive.TarWithOptions(buildDir, &archive.TarOptions{}) + if err != nil { + return "", "", fmt.Errorf("erreur lors de la création du contexte: %w", err) + } + defer buildContextTar.Close() + + // Préparer les options de build + buildOptions := types.ImageBuildOptions{ + Dockerfile: filepath.Base(dockerfilePath), + Tags: spec.BuildConfig.Tags, + Remove: true, + NoCache: spec.BuildConfig.NoCache, + BuildArgs: make(map[string]*string), + } + + // Ajouter les arguments de build + for k, v := range spec.BuildConfig.Args { + value := v // Copie locale pour éviter les problèmes de pointeur + buildOptions.BuildArgs[k] = &value + } + + // Ajouter le target si spécifié + if spec.BuildConfig.Target != "" { + buildOptions.Target = spec.BuildConfig.Target + } + + // Exécuter le build + buildResponse, err := s.dockerClient.ImageBuild(ctx, buildContextTar, buildOptions) + if err != nil { + return "", "", fmt.Errorf("erreur lors du build Docker: %w", err) + } + defer buildResponse.Body.Close() + + // Lire et traiter la sortie + var imageID string + decoder := json.NewDecoder(buildResponse.Body) + for { + var msg jsonmessage.JSONMessage + if err := decoder.Decode(&msg); err != nil { + if err == io.EOF { + break + } + return "", logBuffer.String(), err + } + + // Traiter les messages + if msg.Error != nil { + return "", logBuffer.String(), fmt.Errorf("%s", msg.Error.Message) + } + + if msg.Stream != "" { + fmt.Fprint(&logBuffer, msg.Stream) + // Extraire l'ID de l'image + if strings.Contains(msg.Stream, "Successfully built") { + parts := strings.Fields(msg.Stream) + for i, part := range parts { + if part == "built" && i < len(parts)-1 { + imageID = parts[i+1] + break + } + } + } + } + + // Traiter les messages auxiliaires + if msg.Aux != nil { + var auxMsg struct { + ID string `json:"ID"` + } + if err := json.Unmarshal(*msg.Aux, &auxMsg); err == nil && auxMsg.ID != "" { + imageID = auxMsg.ID + } + } + } + + return imageID, logBuffer.String(), nil +} + +// getImageSize récupère la taille d'une image Docker +func (s *BuildService) getImageSize(ctx context.Context, imageID string) (int64, error) { + summary, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) + if err != nil { + return 0, err + } + return summary.Size, nil +} + +// exportAndUploadImage exporte une image Docker et l'upload vers B2 +func (s *BuildService) exportAndUploadImage(ctx context.Context, imageID string, spec *BuildSpec) ([]string, error) { + if s.b2Config == nil { + return nil, fmt.Errorf("configuration B2 non définie") + } + + // Créer un reader pour l'image exportée + reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) + if err != nil { + return nil, fmt.Errorf("erreur lors de l'export de l'image: %w", err) + } + defer reader.Close() + + // Lire l'image en mémoire + imageData, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("erreur lors de la lecture de l'image: %w", err) + } + + // Initialiser le client B2 + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + + b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) + if err != nil { + return nil, fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + } + + // Accéder au bucket + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + if err != nil { + panic(err) + } + + // Générer un nom d'objet basé sur l'ID de l'image et les tags + imageName := fmt.Sprintf("%s-%s.tar", spec.Name, spec.Version) + objectPath := filepath.Join(s.b2Config.BasePath, imageName) + + // Uploader l'image + obj := bucket.Object(objectPath) + writer := obj.NewWriter(ctx) + + if _, err := writer.Write(imageData); err != nil { + writer.Close() + return nil, fmt.Errorf("erreur lors de l'écriture vers B2: %w", err) + } + + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("erreur lors de la finalisation de l'upload B2: %w", err) + } + + // Pour chaque tag, exporter aussi une référence + objectNames := []string{objectPath} + for _, tag := range spec.BuildConfig.Tags { + // Nettoyer le tag pour en faire un nom de fichier valide + cleanTag := strings.ReplaceAll(tag, ":", "-") + cleanTag = strings.ReplaceAll(cleanTag, "/", "-") + + tagPath := filepath.Join(s.b2Config.BasePath, cleanTag+".tar") + + // Créer un lien symbolique/référence dans B2 + // Note: B2 ne supporte pas directement les liens symboliques, on pourrait soit: + // 1. Uploader à nouveau l'image (gaspillage d'espace) + // 2. Créer un petit fichier de référence qui pointe vers l'image principale + + refObj := bucket.Object(tagPath) + refWriter := refObj.NewWriter(ctx) + + refContent := fmt.Sprintf("Reference to: %s\nImage ID: %s\n", objectPath, imageID) + if _, err := refWriter.Write([]byte(refContent)); err != nil { + refWriter.Close() + return objectNames, err // Retourner les objets déjà uploadés + } + + if err := refWriter.Close(); err != nil { + return objectNames, err + } + + objectNames = append(objectNames, tagPath) + } + + return objectNames, nil +} + +// CreateContainer crée un conteneur à partir d'une image +func (s *BuildService) CreateContainer(ctx context.Context, imageID string, config *container.Config, hostConfig *container.HostConfig) (string, error) { + if config == nil { + config = &container.Config{ + Image: imageID, + } + } else if config.Image == "" { + config.Image = imageID + } + + resp, err := s.dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, "") + if err != nil { + return "", err + } + + return resp.ID, nil +} + +// RunContainer démarre un conteneur et attend sa terminaison +func (s *BuildService) RunContainer(ctx context.Context, containerID string) (string, error) { + if err := s.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + return "", err + } + + // Attendre que le conteneur se termine + statusCh, errCh := s.dockerClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) + + var logs bytes.Buffer + + // Récupérer les logs du conteneur + logsOptions := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + } + + logsReader, err := s.dockerClient.ContainerLogs(ctx, containerID, logsOptions) + if err != nil { + return "", err + } + defer logsReader.Close() + + // Copier les logs dans notre buffer + _, err = stdcopy.StdCopy(&logs, &logs, logsReader) + if err != nil { + return logs.String(), err + } + + // Attendre la fin du conteneur + select { + case err := <-errCh: + return logs.String(), err + case <-statusCh: + return logs.String(), nil + } +} + +// ExportContainer exporte un conteneur vers un fichier tar +func (s *BuildService) ExportContainer(ctx context.Context, containerID string) ([]byte, error) { + // Exporter le conteneur + readCloser, err := s.dockerClient.ContainerExport(ctx, containerID) + if err != nil { + return nil, err + } + defer readCloser.Close() + + // Lire tous les données du conteneur + return io.ReadAll(readCloser) +} + +// Cleanup nettoie les ressources Docker (images, conteneurs) +func (s *BuildService) Cleanup(ctx context.Context, olderThan time.Duration) error { + // Supprimer les conteneurs arrêtés + containers, err := s.dockerClient.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return err + } + + now := time.Now() + for _, c := range containers { + createdTime := time.Unix(c.Created, 0) + if now.Sub(createdTime) > olderThan { + // Supprimer le conteneur + err := s.dockerClient.ContainerRemove(ctx, c.ID, container.RemoveOptions{ + Force: true, + }) + if err != nil { + // Log l'erreur mais continuer + fmt.Printf("Erreur lors de la suppression du conteneur %s: %v\n", c.ID, err) + } + } + } + + // Supprimer les images non utilisées + pruneReport, err := s.dockerClient.ImagesPrune(ctx, filters.NewArgs( + filters.Arg("until", fmt.Sprintf("%dm", int(olderThan.Minutes()))), + )) + if err != nil { + return err + } + + fmt.Printf("Images nettoyées: %d, espace libéré: %d bytes\n", + len(pruneReport.ImagesDeleted), pruneReport.SpaceReclaimed) + + return nil +} + +// UploadArtifactToB2 upload un artefact vers B2 +func (s *BuildService) UploadArtifactToB2(ctx context.Context, data []byte, objectPath string) (string, error) { + if s.b2Config == nil { + return "", fmt.Errorf("configuration B2 non définie") + } + + // Initialiser le client B2 + b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) + if err != nil { + return "", fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + } + + // Accéder au bucket + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + if err != nil { + panic(err) + } + + // Préparer le chemin complet + fullPath := filepath.Join(s.b2Config.BasePath, objectPath) + + // Uploader l'artefact + obj := bucket.Object(fullPath) + writer := obj.NewWriter(ctx) + + // Définir le type de contenu si possible + // if ext := filepath.Ext(objectPath); ext != "" { + // switch strings.ToLower(ext) { + // case ".tar": + // writer.SetContentType("application/x-tar") + // case ".gz", ".tgz": + // writer.SetContentType("application/gzip") + // case ".zip": + // writer.SetContentType("application/zip") + // case ".json": + // writer.SetContentType("application/json") + // case ".txt": + // writer.SetContentType("text/plain") + // } + // } + + if _, err := writer.Write(data); err != nil { + writer.Close() + return "", fmt.Errorf("erreur lors de l'écriture vers B2: %w", err) + } + + if err := writer.Close(); err != nil { + return "", fmt.Errorf("erreur lors de la finalisation de l'upload B2: %w", err) + } + + return fullPath, nil +} + +// DownloadImageFromB2 télécharge une image Docker depuis B2 +func (s *BuildService) DownloadImageFromB2(ctx context.Context, objectPath string) (string, error) { + if s.b2Config == nil { + return "", fmt.Errorf("configuration B2 non définie") + } + + // Initialiser le client B2 + b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) + if err != nil { + return "", fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + } + + // Accéder au bucket + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + if err != nil { + panic(err) + } + + // Préparer le chemin complet + fullPath := objectPath + if !strings.HasPrefix(objectPath, s.b2Config.BasePath) { + fullPath = filepath.Join(s.b2Config.BasePath, objectPath) + } + + // Télécharger l'objet + obj := bucket.Object(fullPath) + reader := obj.NewReader(ctx) + defer reader.Close() + + // Lire l'objet en mémoire + imageData, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("erreur lors de la lecture depuis B2: %w", err) + } + + // Charger l'image dans Docker + loadResp, err := s.dockerClient.ImageLoad(ctx, bytes.NewReader(imageData)) + if err != nil { + return "", fmt.Errorf("erreur lors du chargement de l'image: %w", err) + } + defer loadResp.Body.Close() + + // Lire la réponse pour obtenir l'ID de l'image + var loadOutput struct { + Stream string `json:"stream"` + } + + decoder := json.NewDecoder(loadResp.Body) + imageID := "" + + for { + if err := decoder.Decode(&loadOutput); err != nil { + if err == io.EOF { + break + } + return "", fmt.Errorf("erreur lors de la lecture de la réponse: %w", err) + } + + // Extraire l'ID de l'image si présent + if strings.Contains(loadOutput.Stream, "Loaded image") { + parts := strings.Fields(loadOutput.Stream) + if len(parts) > 2 { + imageID = parts[len(parts)-1] + imageID = strings.TrimSpace(imageID) + break + } + } + } + + // Si on n'a pas trouvé d'ID explicite, lister les images pour trouver la plus récente + if imageID == "" { + images, err := s.dockerClient.ImageList(ctx, image.ListOptions{}) + if err != nil { + return "", fmt.Errorf("erreur lors de la liste des images: %w", err) + } + + if len(images) > 0 { + // Prendre la plus récente (la première de la liste) + imageID = images[0].ID + } + } + + return imageID, nil +} + +// BuildWithMultipleCodebases construit une image Docker à partir de plusieurs codebases +func (s *BuildService) BuildWithMultipleCodebases(ctx context.Context, spec *BuildSpec) (*BuildResult, error) { + // Validation des paramètres + if len(spec.Codebases) < 2 { + return nil, fmt.Errorf("au moins deux codebases sont nécessaires pour cette opération") + } + + return s.Build(ctx, spec) +} + +// SaveDockerImageToBuffer exporte une image Docker vers un buffer en mémoire +func (s *BuildService) SaveDockerImageToBuffer(ctx context.Context, imageID string) ([]byte, error) { + // Exporter l'image + reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) + if err != nil { + return nil, fmt.Errorf("erreur lors de l'export de l'image: %w", err) + } + defer reader.Close() + + // Lire les données en mémoire + var buf bytes.Buffer + _, err = io.Copy(&buf, reader) + if err != nil { + return nil, fmt.Errorf("erreur lors de la lecture des données d'image: %w", err) + } + + return buf.Bytes(), nil +} + +// CreateDockerfileFromMultipleCodebases crée un Dockerfile optimisé pour combiner plusieurs bases de code +func (s *BuildService) CreateDockerfileFromMultipleCodebases(spec *BuildSpec) (string, error) { + if len(spec.Codebases) < 1 { + return "", fmt.Errorf("au moins une codebase est nécessaire") + } + + var dockerfile strings.Builder + + // Si un Dockerfile personnalisé est fourni, l'utiliser tel quel + if spec.BuildConfig.Dockerfile != "" { + return spec.BuildConfig.Dockerfile, nil + } + + // Si aucun Dockerfile personnalisé n'est fourni, en créer un qui combine les codebases + // Utiliser l'image de base spécifiée ou une image par défaut + baseImage := spec.BuildConfig.BaseImage + if baseImage == "" { + baseImage = "alpine:latest" // Image par défaut + } + + dockerfile.WriteString(fmt.Sprintf("FROM %s\n\n", baseImage)) + dockerfile.WriteString("WORKDIR /app\n\n") + + // Ajouter chaque codebase + for _, codebase := range spec.Codebases { + dockerfile.WriteString(fmt.Sprintf("# Ajout de la codebase %s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ /app/%s/\n\n", codebase.Name, codebase.Name)) + } + + // Exposer le port 8080 par défaut + dockerfile.WriteString("EXPOSE 8080\n\n") + + // Commande par défaut + dockerfile.WriteString("CMD [\"sh\", \"-c\", \"echo 'Image construite avec succès à partir de multiples codebases'\"]\n") + + return dockerfile.String(), nil +} + +// BuildWithBufferInput construit une image Docker à partir de données en mémoire +func (s *BuildService) BuildWithBufferInput(ctx context.Context, dockerfile string, contextData map[string][]byte, tags []string) (*BuildResult, error) { + tmpDir, err := ioutil.TempDir("", "docker-build-buffer-") + if err != nil { + return nil, fmt.Errorf("impossible de créer un répertoire temporaire: %w", err) + } + defer os.RemoveAll(tmpDir) + + // Créer le Dockerfile + dockerfilePath := filepath.Join(tmpDir, "Dockerfile") + if err := ioutil.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil { + return nil, fmt.Errorf("erreur lors de la création du Dockerfile: %w", err) + } + + // Créer les fichiers de contexte + for filePath, content := range contextData { + fullPath := filepath.Join(tmpDir, filePath) + + // Créer les répertoires parents + dirPath := filepath.Dir(fullPath) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return nil, fmt.Errorf("erreur lors de la création du répertoire %s: %w", dirPath, err) + } + + // Écrire le fichier + if err := ioutil.WriteFile(fullPath, content, 0644); err != nil { + return nil, fmt.Errorf("erreur lors de l'écriture du fichier %s: %w", filePath, err) + } + } + + // Créer une spécification de build minimale + spec := &BuildSpec{ + Name: "buffer-build", + Version: time.Now().Format("20060102-150405"), + BuildConfig: BuildConfig{ + Tags: tags, + }, + } + + // Construire l'image + imageID, logs, err := s.buildWithDocker(ctx, tmpDir, dockerfilePath, spec) + if err != nil { + return &BuildResult{ + Success: false, + Logs: logs, + ErrorMessage: err.Error(), + }, err + } + + // Récupérer la taille de l'image + imageSize, _ := s.getImageSize(ctx, imageID) + + return &BuildResult{ + Success: true, + ImageID: imageID, + ImageSize: imageSize, + Logs: logs, + }, nil +} + +// GetImageInfo récupère des informations détaillées sur une image Docker +func (s *BuildService) GetImageInfo(ctx context.Context, imageID string) (*types.ImageInspect, error) { + inspect, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) + if err != nil { + return nil, err + } + return &inspect, nil +} + +// TagImage ajoute un tag à une image Docker +func (s *BuildService) TagImage(ctx context.Context, imageID string, tag string) error { + return s.dockerClient.ImageTag(ctx, imageID, tag) +} + +// PushImage pousse une image vers un registry Docker +func (s *BuildService) PushImage(ctx context.Context, tag string, auth string) (string, error) { + var authConfig interface{} + + // Si auth est fourni, le décoder + if auth != "" { + authJSON, err := base64.StdEncoding.DecodeString(auth) + if err != nil { + return "", fmt.Errorf("erreur lors du décodage de l'authentification: %w", err) + } + + if err := json.Unmarshal(authJSON, &authConfig); err != nil { + return "", fmt.Errorf("erreur lors du parsing de l'authentification: %w", err) + } + } + + // Encoder l'authentification pour l'API Docker + encodedAuth, err := json.Marshal(authConfig) + if err != nil { + return "", fmt.Errorf("erreur lors de l'encodage de l'authentification: %w", err) + } + authStr := base64.URLEncoding.EncodeToString(encodedAuth) + + // Options de push + options := image.PushOptions{ + RegistryAuth: authStr, + } + + // Push l'image + pushResp, err := s.dockerClient.ImagePush(ctx, tag, options) + if err != nil { + return "", fmt.Errorf("erreur lors du push de l'image: %w", err) + } + defer pushResp.Close() + + // Lire les logs du push + var logs bytes.Buffer + decoder := json.NewDecoder(pushResp) + + for { + var msg jsonmessage.JSONMessage + if err := decoder.Decode(&msg); err != nil { + if err == io.EOF { + break + } + return logs.String(), fmt.Errorf("erreur lors de la lecture des logs: %w", err) + } + + if msg.Error != nil { + return logs.String(), fmt.Errorf("erreur dans le push: %s", msg.Error.Message) + } + + if msg.Status != "" { + fmt.Fprintf(&logs, "%s: %s\n", time.Now().Format("15:04:05"), msg.Status) + } + if msg.Progress != nil { + fmt.Fprintf(&logs, "%s\n", msg.Progress.String()) + } + } + + return logs.String(), nil +} + +// CompressAndUploadArtifacts compresse les artefacts et les upload vers B2 +func (s *BuildService) CompressAndUploadArtifacts(ctx context.Context, artifacts map[string][]byte, name string) (string, error) { + if len(artifacts) == 0 { + return "", fmt.Errorf("aucun artefact à compresser") + } + + // Créer une archive tar.gz en mémoire + var buf bytes.Buffer + gzw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gzw) + + // Ajouter chaque artefact + for path, data := range artifacts { + header := &tar.Header{ + Name: path, + Mode: 0644, + Size: int64(len(data)), + } + + if err := tw.WriteHeader(header); err != nil { + return "", fmt.Errorf("erreur lors de l'écriture de l'en-tête tar: %w", err) + } + + if _, err := tw.Write(data); err != nil { + return "", fmt.Errorf("erreur lors de l'écriture des données tar: %w", err) + } + } + + // Fermer les writers + if err := tw.Close(); err != nil { + return "", err + } + if err := gzw.Close(); err != nil { + return "", err + } + + // Générer un nom pour l'archive + archiveName := fmt.Sprintf("%s-artifacts-%s.tar.gz", name, time.Now().Format("20060102-150405")) + + // Upload vers B2 + return s.UploadArtifactToB2(ctx, buf.Bytes(), archiveName) +} + +// ExecuteInContainer exécute une commande dans un conteneur temporaire basé sur une image +func (s *BuildService) ExecuteInContainer(ctx context.Context, imageID string, cmd []string, env []string) (string, error) { + // Configuration du conteneur + config := &container.Config{ + Image: imageID, + Cmd: cmd, + Env: env, + Tty: false, + AttachStdout: true, + AttachStderr: true, + } + + // Créer le conteneur + resp, err := s.dockerClient.ContainerCreate(ctx, config, nil, nil, nil, "") + if err != nil { + return "", fmt.Errorf("erreur lors de la création du conteneur: %w", err) + } + containerID := resp.ID + + // Nettoyer le conteneur à la fin + defer s.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{ + Force: true, + }) + + // Démarrer le conteneur + if err := s.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + return "", fmt.Errorf("erreur lors du démarrage du conteneur: %w", err) + } + + // Attendre que le conteneur termine + statusCh, errCh := s.dockerClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) + + var status container.WaitResponse + select { + case err := <-errCh: + return "", fmt.Errorf("erreur lors de l'attente du conteneur: %w", err) + case status = <-statusCh: + // Continuer + } + + // Récupérer les logs + out, err := s.dockerClient.ContainerLogs(ctx, containerID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + if err != nil { + return "", fmt.Errorf("erreur lors de la récupération des logs: %w", err) + } + defer out.Close() + + // Lire les logs + var output bytes.Buffer + _, err = stdcopy.StdCopy(&output, &output, out) + if err != nil { + return "", fmt.Errorf("erreur lors de la lecture des logs: %w", err) + } + + // Vérifier le code de sortie + if status.StatusCode != 0 { + return output.String(), fmt.Errorf("commande terminée avec code de sortie %d", status.StatusCode) + } + + return output.String(), nil +} + +// GenerateMultistageDockerfile génère un Dockerfile multistage pour combiner plusieurs codebases +func (s *BuildService) GenerateMultistageDockerfile(spec *BuildSpec) (string, error) { + if len(spec.Codebases) == 0 { + return "", fmt.Errorf("aucune codebase spécifiée") + } + + var dockerfile strings.Builder + + // Détecter les types de projets dans chaque codebase + // Cette fonction simplifiée détecte Go, Node.js, Python, et Java + projectTypes := make(map[string]string) + for _, codebase := range spec.Codebases { + // Dans une implémentation réelle, cette détection serait plus sophistiquée + if codebase.Path != "" && codebase.Path != "." { + projectTypes[codebase.Name] = "unknown" // Par défaut + } else { + projectTypes[codebase.Name] = "unknown" // Par défaut + } + } + + // Générer les étapes de build pour chaque codebase + for _, codebase := range spec.Codebases { + dockerfile.WriteString(fmt.Sprintf("# Étape de build pour %s\n", codebase.Name)) + + // Déterminer l'image de base selon le type de projet + switch projectTypes[codebase.Name] { + case "go": + dockerfile.WriteString(fmt.Sprintf("FROM golang:1.19-alpine AS %s-builder\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) + dockerfile.WriteString("RUN go build -o app .\n\n") + case "nodejs": + dockerfile.WriteString(fmt.Sprintf("FROM node:18-alpine AS %s-builder\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) + dockerfile.WriteString("RUN npm ci && npm run build\n\n") + case "python": + dockerfile.WriteString(fmt.Sprintf("FROM python:3.10-slim AS %s-builder\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) + dockerfile.WriteString("RUN pip install -r requirements.txt && python setup.py build\n\n") + case "java": + dockerfile.WriteString(fmt.Sprintf("FROM maven:3.8-openjdk-17 AS %s-builder\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) + dockerfile.WriteString("RUN mvn package -DskipTests\n\n") + default: + // Base générique pour les projets inconnus + dockerfile.WriteString(fmt.Sprintf("FROM alpine:latest AS %s-builder\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) + dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n\n", codebase.Name)) + } + } + + // Image finale + baseImage := spec.BuildConfig.BaseImage + if baseImage == "" { + baseImage = "alpine:latest" + } + + dockerfile.WriteString("# Image finale\n") + dockerfile.WriteString(fmt.Sprintf("FROM %s\n", baseImage)) + dockerfile.WriteString("WORKDIR /app\n\n") + + // Copier les artefacts de chaque étape de build + for _, codebase := range spec.Codebases { + switch projectTypes[codebase.Name] { + case "go": + dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/app /app/%s/\n", + codebase.Name, codebase.Name, codebase.Name)) + case "nodejs": + dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/dist /app/%s/\n", + codebase.Name, codebase.Name, codebase.Name)) + case "python": + dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/build /app/%s/\n", + codebase.Name, codebase.Name, codebase.Name)) + case "java": + dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/target/*.jar /app/%s/\n", + codebase.Name, codebase.Name, codebase.Name)) + default: + dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/ /app/%s/\n", + codebase.Name, codebase.Name, codebase.Name)) + } + } + + // Exposer le port 8080 par défaut + dockerfile.WriteString("\nEXPOSE 8080\n\n") + + // Commande par défaut + dockerfile.WriteString("CMD [\"sh\", \"-c\", \"echo 'Image multi-stage construite avec succès'\"]\n") + + return dockerfile.String(), nil +} + +// SetupDockerIgnore crée un fichier .dockerignore pour exclure les fichiers inutiles +func (s *BuildService) SetupDockerIgnore(buildDir string) error { + dockerignore := `# Fichiers de système +.DS_Store +Thumbs.db + +# Répertoires de dépendances +**/node_modules +**/vendor +**/.venv +**/env +**/venv +**/__pycache__ + +# Fichiers de build +**/dist +**/build +**/out +**/bin +**/target +**/.next +**/.nuxt + +# Fichiers de développement +**/.git +**/.github +**/.vscode +**/.idea +**/*.log +**/*.swp +**/*.swo +**/coverage +**/tmp + +# Fichiers de configuration Docker +Dockerfile* +docker-compose* +.dockerignore + +# Fichiers de test +**/*_test.go +**/test +**/tests +**/*.spec.js +**/*.test.js +` + + return os.WriteFile(filepath.Join(buildDir, ".dockerignore"), []byte(dockerignore), 0644) +} + +// GenerateImageReport crée un rapport détaillé sur une image Docker +func (s *BuildService) GenerateImageReport(ctx context.Context, imageID string) (map[string]interface{}, error) { + report := make(map[string]interface{}) + + // Informations générales sur l'image + inspect, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) + if err != nil { + return nil, fmt.Errorf("erreur lors de l'inspection de l'image: %w", err) + } + + report["image_id"] = inspect.ID + report["created"] = inspect.Created + report["size"] = inspect.Size + report["virtual_size"] = inspect.VirtualSize + report["tags"] = inspect.RepoTags + report["labels"] = inspect.Config.Labels + report["entrypoint"] = inspect.Config.Entrypoint + report["cmd"] = inspect.Config.Cmd + report["env"] = inspect.Config.Env + report["exposed_ports"] = inspect.Config.ExposedPorts + report["os"] = inspect.Os + report["architecture"] = inspect.Architecture + + // Récupérer l'historique de l'image + history, err := s.dockerClient.ImageHistory(ctx, imageID) + if err != nil { + return report, fmt.Errorf("erreur lors de la récupération de l'historique: %w", err) + } + + historyInfo := make([]map[string]interface{}, 0, len(history)) + for _, layer := range history { + historyInfo = append(historyInfo, map[string]interface{}{ + "created": layer.Created, + "created_by": layer.CreatedBy, + "size": layer.Size, + "comment": layer.Comment, + "tags": layer.Tags, + }) + } + report["history"] = historyInfo + + return report, nil +} diff --git a/bx/go.mod b/bx/go.mod new file mode 100644 index 0000000..85c7689 --- /dev/null +++ b/bx/go.mod @@ -0,0 +1,43 @@ +module anexis/bx + +go 1.24.2 + +require ( + github.com/docker/docker v28.1.1+incompatible + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Backblaze/blazer v0.7.2 + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/time v0.11.0 // indirect +) diff --git a/bx/go.sum b/bx/go.sum new file mode 100644 index 0000000..0a6ef2b --- /dev/null +++ b/bx/go.sum @@ -0,0 +1,159 @@ +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM= +github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/bx/types.go b/bx/types.go new file mode 100644 index 0000000..b953231 --- /dev/null +++ b/bx/types.go @@ -0,0 +1,15 @@ +package types + +type BuildSpec struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Build []struct { + Path string `yaml:"path"` + Dockerfile string `yaml:"dockerfile"` + Tag string `yaml:"tag"` + } `yaml:"build"` + Output []struct { + Type string `yaml:"type"` + Name string `yaml:"name"` + } `yaml:"output"` +} diff --git a/package.json b/frontend/package.json similarity index 74% rename from package.json rename to frontend/package.json index 2c8086f..a0b5a43 100644 --- a/package.json +++ b/frontend/package.json @@ -4,9 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "tailwind": "postcss ./frontend/input.css -o ./client/output.css", - "tailwindDev": "postcss ./frontend/input.css -o ./client/output.css --watch" + "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", diff --git a/pnpm-lock.yaml b/frontend/pnpm-lock.yaml similarity index 100% rename from pnpm-lock.yaml rename to frontend/pnpm-lock.yaml diff --git a/tsconfig.json b/frontend/tsconfig.json similarity index 93% rename from tsconfig.json rename to frontend/tsconfig.json index 33451fd..dba692c 100644 --- a/tsconfig.json +++ b/frontend/tsconfig.json @@ -15,5 +15,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["./frontend/src"] + "include": ["./src"] } diff --git a/vite.config.ts b/frontend/vite.config.ts similarity index 100% rename from vite.config.ts rename to frontend/vite.config.ts diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 12a703d..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/api/routes.go b/server/api/routes.go similarity index 85% rename from api/routes.go rename to server/api/routes.go index e896dbb..20e8f71 100644 --- a/api/routes.go +++ b/server/api/routes.go @@ -3,9 +3,9 @@ package api import ( "log" - "cloudbeast.doni/m/controllers" - "cloudbeast.doni/m/middleware" - "cloudbeast.doni/m/routes" + "anexis/server/controllers" + "anexis/server/middleware" + "anexis/server/routes" "github.com/gin-gonic/gin" ) diff --git a/atlas.hcl b/server/atlas.hcl similarity index 100% rename from atlas.hcl rename to server/atlas.hcl diff --git a/controllers/auth_controller.go b/server/controllers/auth_controller.go similarity index 95% rename from controllers/auth_controller.go rename to server/controllers/auth_controller.go index 085fe40..09f15c9 100644 --- a/controllers/auth_controller.go +++ b/server/controllers/auth_controller.go @@ -4,8 +4,8 @@ import ( "net/http" "time" - "cloudbeast.doni/m/db" - "cloudbeast.doni/m/models" + "anexis/server/db" + "anexis/server/models" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" ) diff --git a/controllers/file_controller.go b/server/controllers/file_controller.go similarity index 100% rename from controllers/file_controller.go rename to server/controllers/file_controller.go diff --git a/controllers/test_controller.go b/server/controllers/test_controller.go similarity index 100% rename from controllers/test_controller.go rename to server/controllers/test_controller.go diff --git a/db/db.go b/server/db/db.go similarity index 100% rename from db/db.go rename to server/db/db.go diff --git a/default.pgo b/server/default.pgo similarity index 100% rename from default.pgo rename to server/default.pgo diff --git a/go.mod b/server/go.mod similarity index 69% rename from go.mod rename to server/go.mod index d422093..3913afb 100644 --- a/go.mod +++ b/server/go.mod @@ -1,10 +1,9 @@ -module cloudbeast.doni/m +module anexis/server go 1.23.5 require ( ariga.io/atlas-provider-gorm v0.5.0 - github.com/docker/docker v27.4.1+incompatible github.com/gin-contrib/pprof v1.5.2 github.com/gin-gonic/gin v1.10.0 github.com/joho/godotenv v1.5.1 @@ -18,8 +17,10 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/zerolog v1.33.0 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/tkrajina/typescriptify-golang-structs v0.2.0 // indirect @@ -28,16 +29,7 @@ require ( require ( ariga.io/atlas-go-sdk v0.6.5 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Microsoft/go-winio v0.4.14 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -46,25 +38,9 @@ require ( github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/microsoft/go-mssqldb v1.8.0 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.9.3 - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/time v0.8.0 // indirect gorm.io/driver/mysql v1.5.2 // indirect gorm.io/driver/sqlserver v1.5.2 // indirect - gotest.tools/v3 v3.5.1 // indirect ) require ( @@ -74,7 +50,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/evanw/esbuild v0.24.2 + github.com/evanw/esbuild v0.24.2 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect diff --git a/go.sum b/server/go.sum similarity index 75% rename from go.sum rename to server/go.sum index 9c87664..7ab5680 100644 --- a/go.sum +++ b/server/go.sum @@ -24,16 +24,12 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1: github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM= github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= @@ -41,34 +37,21 @@ github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= -github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A= github.com/evanw/esbuild v0.24.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= @@ -79,11 +62,6 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -98,8 +76,6 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -119,8 +95,6 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -145,13 +119,10 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -171,10 +142,6 @@ github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -182,37 +149,26 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/natewong1313/go-react-ssr v0.1.15 h1:vycZXFFnsZfxCaGKLJKM3oTeRzVfEtHHaHnSSHVmUHE= github.com/natewong1313/go-react-ssr v0.1.15/go.mod h1:VRY3QQsqo6Yhrd7d26a4nXkaYeQdOKF95bsSBPj/riY= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -232,31 +188,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -265,16 +200,12 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -285,16 +216,12 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -325,24 +252,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -367,6 +281,4 @@ gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqw gorm.io/gorm v1.25.2-0.20230610234218-206613868439/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/cmd/loader/main.go b/server/loader/main.go similarity index 91% rename from cmd/loader/main.go rename to server/loader/main.go index 0bf88f8..7f6e401 100644 --- a/cmd/loader/main.go +++ b/server/loader/main.go @@ -6,7 +6,7 @@ import ( "os" "ariga.io/atlas-provider-gorm/gormschema" - "cloudbeast.doni/m/models" + "anexis/server/models" ) func main() { diff --git a/cmd/main.go b/server/main.go similarity index 94% rename from cmd/main.go rename to server/main.go index 7e34b69..0d978d6 100644 --- a/cmd/main.go +++ b/server/main.go @@ -5,8 +5,9 @@ import ( "log" "os" - "cloudbeast.doni/m/api" - "cloudbeast.doni/m/utils" + "anexis/server/api" + "anexis/utils" + "github.com/Backblaze/blazer/b2" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" diff --git a/middleware/auth.go b/server/middleware/auth.go similarity index 95% rename from middleware/auth.go rename to server/middleware/auth.go index d30e44a..53de9cf 100644 --- a/middleware/auth.go +++ b/server/middleware/auth.go @@ -3,7 +3,7 @@ package middleware import ( "net/http" - "cloudbeast.doni/m/controllers" + "anexis/server/controllers" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" ) diff --git a/migrations/20241231112547.sql b/server/migrations/20241231112547.sql similarity index 100% rename from migrations/20241231112547.sql rename to server/migrations/20241231112547.sql diff --git a/migrations/atlas.sum b/server/migrations/atlas.sum similarity index 100% rename from migrations/atlas.sum rename to server/migrations/atlas.sum diff --git a/models/models.go b/server/models/models.go similarity index 100% rename from models/models.go rename to server/models/models.go diff --git a/models/props.go b/server/models/props.go similarity index 100% rename from models/props.go rename to server/models/props.go diff --git a/routes/main.go b/server/routes/main.go similarity index 93% rename from routes/main.go rename to server/routes/main.go index 0f7463b..b336073 100644 --- a/routes/main.go +++ b/server/routes/main.go @@ -1,7 +1,7 @@ package routes import ( - "cloudbeast.doni/m/models" + "anexis/server/models" "github.com/gin-gonic/gin" gossr "github.com/natewong1313/go-react-ssr" ) diff --git a/routes/static.go b/server/routes/static.go similarity index 100% rename from routes/static.go rename to server/routes/static.go diff --git a/cmd/server_test.go b/server/server_test.go similarity index 94% rename from cmd/server_test.go rename to server/server_test.go index 01d78d0..da36477 100644 --- a/cmd/server_test.go +++ b/server/server_test.go @@ -5,7 +5,7 @@ import ( "net/http/httptest" "testing" - "cloudbeast.doni/m/api" + "anexis/server/api" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) diff --git a/services/builder_service.go b/services/builder_service.go new file mode 100644 index 0000000..183ba73 --- /dev/null +++ b/services/builder_service.go @@ -0,0 +1,25 @@ +package services + +type BuildMetaData struct { + BaseImage string `json:"base_image"` + Entry []DirEntry `json:"entry"` + Specs map[string]Specs `json:"specs"` +} + +type DirEntry struct { + IsFile bool `json:"is_file"` + IsDir bool `json:"is_dir"` + FileContent []byte `json:"file_content"` + FileName string `json:"file_name"` + Entries []DirEntry `json:"entries"` +} + +type Specs struct { + Type string `json:"type"` +} + +const ( + COMMAND = "Command" + EXPR = "Expression" + ANNOTATION = "Annotation" +) diff --git a/services/encryption_service.go b/services/encryption_service.go index 11077d8..b1ac956 100644 --- a/services/encryption_service.go +++ b/services/encryption_service.go @@ -14,7 +14,8 @@ func EncryptFile(fileData []byte, pubKey *rsa.PublicKey) ([]byte, error) { } func SaveEncryptedFile(filename string, data []byte) error { - return os.WriteFile("/path/to/storage/"+filename, data, 0o644) + storagePath := "/path" + return os.WriteFile(storagePath+filename, data, 0o644) } func DecryptFile(cipherData []byte, privKey *rsa.PrivateKey) ([]byte, error) { diff --git a/services/go.mod b/services/go.mod new file mode 100644 index 0000000..6a11f16 --- /dev/null +++ b/services/go.mod @@ -0,0 +1,37 @@ +module anexis/services + +go 1.24.2 + +require ( + github.com/Backblaze/blazer v0.7.2 + github.com/docker/docker v28.1.1+incompatible + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/time v0.11.0 // indirect + gotest.tools/v3 v3.5.2 // indirect +) diff --git a/services/go.sum b/services/go.sum new file mode 100644 index 0000000..c926c7c --- /dev/null +++ b/services/go.sum @@ -0,0 +1,139 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM= +github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/services/services_test.go b/services/services_test.go index 206d3cd..23ff747 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -5,7 +5,7 @@ import ( "path" "testing" - "cloudbeast.doni/m/services" + "anexis/services" ) const ( diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 477eaf6..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./frontend/src/**/*.{html,tsx}',], - theme: { - extend: {}, - }, - plugins: [ - require('@tailwindcss/typography'), - require('@tailwindcss/forms')({ strategy: 'class' }), - require('@tailwindcss/aspect-ratio'), - require('daisyui'), - require('tailwind-scrollbar-hide'), - ], -}; diff --git a/utils/go.mod b/utils/go.mod new file mode 100644 index 0000000..5c97e67 --- /dev/null +++ b/utils/go.mod @@ -0,0 +1,3 @@ +module anexis/utils + +go 1.24.2 diff --git a/utils/helpers_test.go b/utils/helpers_test.go index 21040a8..8b300b6 100644 --- a/utils/helpers_test.go +++ b/utils/helpers_test.go @@ -4,7 +4,7 @@ import ( "crypto/rsa" "testing" - "cloudbeast.doni/m/utils" + "anexis/utils" ) var privateKey *rsa.PrivateKey From 8623e5d0b88ec6509190523344f94c88a7b5e4fc Mon Sep 17 00:00:00 2001 From: DoniLite Date: Sat, 26 Apr 2025 19:51:24 +0000 Subject: [PATCH 5/8] chore: bx & socket - create new packages `bx`, `socket` and `cli` - implementing new features - fixing some bugs - refactoring the codebase --- .gitignore | 3 +- anexis.spec.yml | 1 + bx/build/build_integration_test.go | 201 +++ bx/build/build_test.go | 819 +++++++++ bx/build/builder.go | 2537 ++++++++++++++++++---------- bx/build/detector.go | 158 ++ bx/build/socket.go | 462 +++++ bx/build/templates.go | 401 +++++ bx/cmd/run.go | 190 +++ bx/go.mod | 40 +- bx/go.sum | 129 +- cli/components/run.go | 214 +++ cli/go.mod | 3 + socket/client.go | 262 +++ socket/conn.go | 178 ++ socket/go.mod | 15 + socket/go.sum | 14 + socket/hub.go | 89 + socket/message.go | 127 ++ socket/server.go | 267 +++ socket/socket_test.go | 214 +++ 21 files changed, 5360 insertions(+), 964 deletions(-) create mode 100644 anexis.spec.yml create mode 100644 bx/build/build_integration_test.go create mode 100644 bx/build/build_test.go create mode 100644 bx/build/detector.go create mode 100644 bx/build/socket.go create mode 100644 bx/build/templates.go create mode 100644 bx/cmd/run.go create mode 100644 cli/components/run.go create mode 100644 cli/go.mod create mode 100644 socket/client.go create mode 100644 socket/conn.go create mode 100644 socket/go.mod create mode 100644 socket/go.sum create mode 100644 socket/hub.go create mode 100644 socket/message.go create mode 100644 socket/server.go create mode 100644 socket/socket_test.go diff --git a/.gitignore b/.gitignore index 04bcb18..d764f33 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ client /**/node_modules test.db bin -rsa/ \ No newline at end of file +rsa/ +/**/**TODO.md \ No newline at end of file diff --git a/anexis.spec.yml b/anexis.spec.yml new file mode 100644 index 0000000..72b3e42 --- /dev/null +++ b/anexis.spec.yml @@ -0,0 +1 @@ +version: 0.1.0 \ No newline at end of file diff --git a/bx/build/build_integration_test.go b/bx/build/build_integration_test.go new file mode 100644 index 0000000..78d372c --- /dev/null +++ b/bx/build/build_integration_test.go @@ -0,0 +1,201 @@ +// build/build_integration_socket_test.go +package build + +import ( + "context" + "fmt" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + // Importer socket et testify etc. + "anexis/socket" // Ajuster le chemin si besoin + + "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// Test d'intégration complet: Client -> Serveur Socket -> BuildService -> Docker +func TestIntegration_SocketTriggeredBuild_LocalOutput(t *testing.T) { + // //go:build integration + skipWithoutDocker(t) // Fonction helper définie dans build_test.go + + // 1. Setup BuildService (avec un mock fetcher simple) + tempDir := t.TempDir() + mockFetcher := &MockSecretFetcher{Secrets: map[string]string{"secret/for/build": "build_secret"}} + buildService, err := NewBuildService(tempDir, false, mockFetcher) + require.NoError(t, err) + + // 2. Setup et démarrer le Serveur Socket + socketServer := socket.NewServer(buildService, buildService) // buildService implémente les deux interfaces + socketServer.Run() + httpServer := httptest.NewServer(socketServer) + defer httpServer.Close() + wsURL := "ws" + strings.TrimPrefix(httpServer.URL, "http") + t.Logf("Integration Test Server running at: %s\n", wsURL) + + // 3. Préparer le BuildSpec et la codebase locale + codeDir := createTempDir(t, tempDir, "appcode") + dockerfileContent := ` +FROM alpine:latest +ARG SECRET_ARG=not_set +ENV BUILT_SECRET=$SECRET_ARG +COPY data.txt / +RUN echo "Built at $(date)" >> /build_time.txt +CMD cat /data.txt && echo "Secret: $BUILT_SECRET" && cat /build_time.txt +` + createTempFile(t, codeDir, "Dockerfile", dockerfileContent) + createTempFile(t, codeDir, "data.txt", "Real build data") + + buildVersion := fmt.Sprintf("sock-0.1-%d", time.Now().Unix()) + buildSpec := &BuildSpec{ + Name: "integ-socket-build", + Version: buildVersion, + Codebases: []CodebaseConfig{ + {Name: "app", SourceType: "local", Source: codeDir}, + }, + BuildConfig: BuildConfig{ + Dockerfile: "app/Dockerfile", // Relatif au buildDir créé par BuildService + Args: map[string]string{"SECRET_ARG": "${SECRET_FROM_ENV}"}, // Sera injecté par runtimeEnv + OutputTarget: "local", // Sortie fichier .tar + Tags: []string{ // Tags même si sortie locale + fmt.Sprintf("integ-socket-build:%s", buildVersion), + }, + }, + RunConfigDef: RunConfigDef{Generate: true, ArtifactStorage: "local"}, + // Secret requis par l'argument de build (sera récupéré via le serveur) + Secrets: []SecretSpec{{Name: "SECRET_FROM_ENV", Source: "secret/for/build"}}, + } + imageTag := buildSpec.BuildConfig.Tags[0] + + // Convertir BuildSpec en YAML pour l'envoyer + specYAMLBytes, err := yaml.Marshal(buildSpec) + require.NoError(t, err) + specYAML := string(specYAMLBytes) + + // 4. Démarrer le Client Socket et se connecter + socketClient := socket.NewClient() + err = socketClient.Connect(wsURL, nil) + require.NoError(t, err) + defer socketClient.Close() + + // 5. Écouter les messages entrants sur le client + clientMessages := make(chan *socket.Message, 20) // Buffer large pour les logs + clientErrors := make(chan error, 1) + go func() { + for msg := range socketClient.Incoming { + clientMessages <- msg + } + close(clientMessages) // Signaler la fin quand le client se ferme + t.Log("Client message listener finished.") + }() + + // 6. Envoyer la requête de build + buildReqPayload := socket.BuildRequestPayload{BuildSpecYAML: specYAML} + ctxReq, cancelReq := context.WithTimeout(context.Background(), 5*time.Second) // Timeout pour recevoir BuildQueued + defer cancelReq() + + respMsg, err := socketClient.SendRequest(ctxReq, socket.EvtBuildRequest, buildReqPayload) + require.NoError(t, err, "Failed to send build request") + require.Equal(t, socket.EvtBuildQueued, respMsg.Type) + + var queuedPayload socket.BuildQueuedPayload + err = respMsg.DecodePayload(&queuedPayload) + require.NoError(t, err) + require.NotEmpty(t, queuedPayload.BuildID) + buildID := queuedPayload.BuildID + t.Logf("Build queued with ID: %s", buildID) + + // 7. Attendre et vérifier les messages de logs et de statut final + var receivedLogs strings.Builder + var finalStatusPayload socket.BuildStatusPayload + receivedFinalStatus := false + buildTimeout := time.After(30 * time.Second) // Timeout pour le build complet + + keepListening := true + for keepListening { + select { + case msg, ok := <-clientMessages: + if !ok { + t.Log("Client message channel closed unexpectedly.") + clientErrors <- fmt.Errorf("client message channel closed before receiving final status") + keepListening = false + break + } + t.Logf("Client Received: Type=%s", msg.Type) + switch msg.Type { + case socket.EvtLogChunk: + var logPayload socket.LogChunkPayload + if err := msg.DecodePayload(&logPayload); err == nil { + require.Equal(t, buildID, logPayload.BuildID) + receivedLogs.WriteString(logPayload.Content) // Concaténer les logs + } else { + t.Logf("Failed to decode log chunk payload: %v", err) + } + case socket.EvtBuildStatus: + var statusPayload socket.BuildStatusPayload + if err := msg.DecodePayload(&statusPayload); err == nil { + require.Equal(t, buildID, statusPayload.BuildID) + t.Logf("Received Status Update: %s (Msg: %s)", statusPayload.Status, statusPayload.Message) + // Vérifier si c'est un statut terminal + if statusPayload.Status == "success" || statusPayload.Status == "failure" { + finalStatusPayload = statusPayload + receivedFinalStatus = true + keepListening = false // Arrêter d'écouter + } + } else { + t.Logf("Failed to decode status payload: %v", err) + } + case socket.EvtError: // Gérer les erreurs générales + t.Logf("Received general error message: %s", msg.Error) + clientErrors <- fmt.Errorf("received error event: %s", msg.Error) + keepListening = false + + } + case <-buildTimeout: + t.Fatalf("Timeout waiting for final build status for BuildID %s", buildID) + case err := <-clientErrors: + t.Fatalf("Error received during build: %v", err) + + } + } + + // 8. Vérifier le résultat + require.True(t, receivedFinalStatus, "Should have received a final build status") + // Vérifier les logs reçus (rechercher des motifs clés) + logs := receivedLogs.String() + t.Logf("--- Received Build Logs ---\n%s\n-------------------------\n", logs) + assert.Contains(t, logs, "Starting build process...") + assert.Contains(t, logs, "Fetching secrets...") // Vient du BuildService + assert.Contains(t, logs, "Successfully built") // Vient de Docker + assert.Contains(t, logs, "Build process completed successfully.") + + // Vérifier le statut final + require.Equal(t, "success", finalStatusPayload.Status, "Final build status should be success. Error: %s", finalStatusPayload.Message) + require.NotNil(t, finalStatusPayload.DurationSec) + assert.True(t, *finalStatusPayload.DurationSec > 0) + require.NotEmpty(t, finalStatusPayload.ArtifactRef, "Artifact reference should not be empty for local output") + assert.True(t, filepath.IsAbs(finalStatusPayload.ArtifactRef), "Local artifact path should be absolute") // runBuildLogic retourne le chemin absolu + + // Vérifier l'existence de l'artefact local (.tar) + _, err = os.Stat(finalStatusPayload.ArtifactRef) + assert.NoError(t, err, "Local artifact file should exist at %s", finalStatusPayload.ArtifactRef) + + // Vérifier le run.yml + runYmlPath := filepath.Join(filepath.Dir(finalStatusPayload.ArtifactRef), fmt.Sprintf("%s-%s.run.yml", buildSpec.Name, buildSpec.Version)) + _, err = os.Stat(runYmlPath) + assert.NoError(t, err, "run.yml file should exist at %s", runYmlPath) + + // 9. Nettoyage Docker (l'image a été buildée même si sortie locale) + cli, _ := client.NewClientWithOpts(client.FromEnv) + defer cli.Close() + t.Cleanup(func() { + removeDockerImage(t, cli, imageTag) + }) + assert.True(t, dockerImageExists(t, cli, imageTag), "Docker image should exist after build") +} \ No newline at end of file diff --git a/bx/build/build_test.go b/bx/build/build_test.go new file mode 100644 index 0000000..0b3ee45 --- /dev/null +++ b/bx/build/build_test.go @@ -0,0 +1,819 @@ +// build_test.go +package build + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + // Go-Git imports pour le repo local de test + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// --- Mocks & Helpers --- + +// MockSecretFetcher pour les tests unitaires +type MockSecretFetcher struct { + Secrets map[string]string + Err error +} + +func (m *MockSecretFetcher) GetSecret(ctx context.Context, source string) (string, error) { + if m.Err != nil { + return "", m.Err + } + val, ok := m.Secrets[source] + if !ok { + return "", fmt.Errorf("secret source '%s' not found in mock", source) + } + return val, nil +} + +// Helper pour créer un fichier temporaire +func createTempFile(t *testing.T, dir, filename, content string) string { + t.Helper() + path := filepath.Join(dir, filename) + err := os.WriteFile(path, []byte(content), 0644) + require.NoError(t, err) + return path +} + +// Helper pour créer un répertoire temporaire +func createTempDir(t *testing.T, parent, name string) string { + t.Helper() + path := filepath.Join(parent, name) + err := os.MkdirAll(path, 0755) + require.NoError(t, err) + return path +} + +// Helper pour vérifier si une image Docker existe +func dockerImageExists(t *testing.T, cli *client.Client, imageRef string) bool { + t.Helper() + _, _, err := cli.ImageInspectWithRaw(context.Background(), imageRef) + return err == nil +} + +// Helper pour supprimer une image Docker (avec force) +func removeDockerImage(t *testing.T, cli *client.Client, imageRef string) { + t.Helper() + if !dockerImageExists(t, cli, imageRef) { + return // N'existe pas déjà + } + _, err := cli.ImageRemove(context.Background(), imageRef, image.RemoveOptions{Force: true, PruneChildren: true}) + // Ne pas faire échouer le test si le remove échoue (peut arriver dans certains cas), juste logguer. + if err != nil { + t.Logf("Warning: failed to remove docker image %s: %v", imageRef, err) + } else { + t.Logf("Successfully removed docker image %s", imageRef) + } +} + +// Helper pour initialiser un dépôt Git local pour les tests +func setupLocalGitRepo(t *testing.T, dir string, files map[string]string) (string, string) { + t.Helper() + repoDir := filepath.Join(dir, "test-repo.git") + err := os.MkdirAll(repoDir, 0755) + require.NoError(t, err) + + // Initialiser le dépôt bare pour simuler un remote + _, err = git.PlainInit(repoDir, true) + require.NoError(t, err) + + // Cloner ce dépôt bare dans un répertoire de travail temporaire + workDir := filepath.Join(dir, "test-repo-work") + repo, err := git.PlainClone(workDir, false, &git.CloneOptions{ + URL: repoDir, // Cloner depuis le bare repo local + }) + require.NoError(t, err) + + // Ajouter des fichiers et commiter + w, err := repo.Worktree() + require.NoError(t, err) + + for name, content := range files { + filename := filepath.Join(workDir, name) + err = os.WriteFile(filename, []byte(content), 0644) + require.NoError(t, err) + _, err = w.Add(name) + require.NoError(t, err) + } + + commit, err := w.Commit("Initial commit for testing", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Test Author", + Email: "test@example.com", + When: time.Now(), + }, + }) + require.NoError(t, err) + + // Push vers le dépôt bare (qui sert de 'remote') + err = repo.Push(&git.PushOptions{}) + // Ignorer "already up-to-date" car on vient de commiter + if err != nil && err != git.NoErrAlreadyUpToDate { + require.NoError(t, err) + } + + // Retourner l'URL du dépôt bare et le hash du commit + return "file://" + repoDir, commit.String() +} + +// --- Tests Unitaires --- + +func TestLoadBuildSpec_ValidYAML(t *testing.T) { + yamlData := ` +name: my-app +version: 1.0.1 +codebases: + - name: backend + source_type: git + source: https://github.com/test/backend.git + branch: main +build_config: + dockerfile: Dockerfile.backend + tags: ["app:latest", "app:1.0.1"] +env: + API_URL: /api/v1 +env_files: + - .env.prod +secrets: + - name: DB_PASSWORD + source: backend/db/password +resources: + - url: http://example.com/data.zip + target_path: backend/data.zip + extract: true +run_config_def: + generate: true + artifact_storage: local +` + spec, err := LoadBuildSpecFromBytes([]byte(yamlData), ".yaml") + require.NoError(t, err) + require.NotNil(t, spec) + + assert.Equal(t, "my-app", spec.Name) + assert.Equal(t, "1.0.1", spec.Version) + require.Len(t, spec.Codebases, 1) + assert.Equal(t, "backend", spec.Codebases[0].Name) + assert.Equal(t, "git", spec.Codebases[0].SourceType) + assert.Equal(t, "Dockerfile.backend", spec.BuildConfig.Dockerfile) + assert.Contains(t, spec.BuildConfig.Tags, "app:latest") + assert.Equal(t, "/api/v1", spec.Env["API_URL"]) + assert.Contains(t, spec.EnvFiles, ".env.prod") + require.Len(t, spec.Secrets, 1) + assert.Equal(t, "DB_PASSWORD", spec.Secrets[0].Name) + assert.Equal(t, "backend/db/password", spec.Secrets[0].Source) + require.Len(t, spec.Resources, 1) + assert.Equal(t, "http://example.com/data.zip", spec.Resources[0].URL) + assert.True(t, spec.Resources[0].Extract) + assert.True(t, spec.RunConfigDef.Generate) + assert.Equal(t, "local", spec.RunConfigDef.ArtifactStorage) +} + +func TestLoadBuildSpec_InvalidFormat(t *testing.T) { + invalidData := `{"name": "test",` // Invalid JSON + _, err := LoadBuildSpecFromBytes([]byte(invalidData), ".json") + require.Error(t, err) +} + +func TestLoadBuildSpec_MissingRequiredFields(t *testing.T) { + yamlData := `version: 1.0.0` // Missing name + _, err := LoadBuildSpecFromBytes([]byte(yamlData), ".yaml") + require.Error(t, err) + assert.Contains(t, err.Error(), "name' et 'version' sont requis") + + yamlData = `name: test` // Missing version + _, err = LoadBuildSpecFromBytes([]byte(yamlData), ".yaml") + require.Error(t, err) + assert.Contains(t, err.Error(), "name' et 'version' sont requis") + + yamlData = `name: test +version: 1.0.0` // Missing codebases/dockerfile/compose + _, err = LoadBuildSpecFromBytes([]byte(yamlData), ".yaml") + require.Error(t, err) + assert.Contains(t, err.Error(), "aucune codebase, build_step, dockerfile ou compose_file") +} + +func TestLoadComposeFile_DirectYAML(t *testing.T) { + composeData := ` +version: '3.8' +services: + web: + build: + context: ./frontend + dockerfile: Dockerfile.prod + args: + NODE_ENV: production + ports: + - "8080:80" + environment: + API_HOST: api_service + TZ: UTC + api: + image: my-api:latest + environment: + DB_HOST: db +` + project, err := LoadComposeFile([]byte(composeData)) + require.NoError(t, err) + require.NotNil(t, project) + + require.Contains(t, project.Services, "web") + require.Contains(t, project.Services, "api") + + webService := project.Services["web"] + require.NotNil(t, webService.Build) + assert.Equal(t, "./frontend", webService.Build.Context) + assert.Equal(t, "Dockerfile.prod", webService.Build.Dockerfile) + require.NotNil(t, webService.Build.Args) + require.Contains(t, webService.Build.Args, "NODE_ENV") + assert.Equal(t, "production", *webService.Build.Args["NODE_ENV"]) + require.Len(t, webService.Ports, 1) + assert.Equal(t, "8080:80", webService.Ports[0]) // Note: compose-go parses ports into a struct + require.NotNil(t, webService.Environment) + assert.Equal(t, "api_service", *webService.Environment["API_HOST"]) + + apiService := project.Services["api"] + assert.Nil(t, apiService.Build) + assert.Equal(t, "my-api:latest", apiService.Image) + require.NotNil(t, apiService.Environment) + assert.Equal(t, "db", *apiService.Environment["DB_HOST"]) +} + +func TestGenerateRunYAML_SimpleDocker(t *testing.T) { + spec := &BuildSpec{ + Name: "my-app", + Version: "1.1.0", + BuildConfig: BuildConfig{ + Tags: []string{"test/app:1.1", "test/app:latest"}, + }, + RunConfigDef: RunConfigDef{ + Generate: true, + ArtifactStorage: "docker", // Important pour la référence d'image + }, + Env: map[string]string{"GLOBAL_VAR": "global_value"}, + Secrets: []SecretSpec{ + {Name: "MY_SECRET", Source: "secret/path"}, + }, + } + result := &BuildResult{ + Success: true, + ImageID: "sha256:abcdef123456", + ImageIDs: map[string]string{"my-app": "sha256:abcdef123456"}, // Simuler le résultat + } + runtimeEnv := map[string]string{ + "GLOBAL_VAR": "global_value", + "MY_SECRET": "secret_value", // Simuler le secret récupéré + } + finalImageTags := map[string][]string{ + "my-app": {"test/app:1.1", "test/app:latest"}, + } + + // Service (qui n'existe pas ici, mais passons quand même un mock) + mockFetcher := &MockSecretFetcher{Secrets: map[string]string{"secret/path": "secret_value"}} + service, err := NewBuildService(t.TempDir(), true, mockFetcher) + require.NoError(t, err) + + var composeProject *ComposeProject = nil + + runYAML, err := service.generateRunYAML(context.Background(), spec, result, runtimeEnv, finalImageTags, composeProject) + require.NoError(t, err) + require.NotNil(t, runYAML) + + assert.Equal(t, "1.0", runYAML.Version) + require.Contains(t, runYAML.Services, "my-app") + + runService := runYAML.Services["my-app"] + assert.Equal(t, "test/app:1.1", runService.Image) // Devrait utiliser le premier tag pour Docker storage + require.NotNil(t, runService.Environment) + assert.Equal(t, "global_value", runService.Environment["GLOBAL_VAR"]) + assert.Equal(t, "secret_value", runService.Environment["MY_SECRET"]) +} + +func TestGenerateRunYAML_ComposeLocal(t *testing.T) { + spec := &BuildSpec{ + Name: "compose-proj", + Version: "dev", + BuildConfig: BuildConfig{ + ComposeFile: "docker-compose.yml", // Important pour déclencher la logique compose + }, + RunConfigDef: RunConfigDef{ + Generate: true, + ArtifactStorage: "local", // Important pour la référence d'image + }, + } + // Simuler un résultat de build compose + result := &BuildResult{ + Success: true, + ImageIDs: map[string]string{ + "web": "sha256:web123", + "api": "sha256:api456", + }, + LocalImagePaths: map[string]string{ // Important pour artifactStorage=local + "web": "/output/compose-proj_web.tar", + "api": "/output/compose-proj_api.tar", + }, + ServiceOutputs: map[string]ServiceOutput{ + "web": {ImageID: "sha256:web123"}, + "api": {ImageID: "sha256:api456"}, + }, + } + runtimeEnv := map[string]string{"GLOBAL": "on"} + finalImageTags := map[string][]string{ // Tags générés par défaut par buildComposeProject + "web": {"compose-proj_web:latest"}, + "api": {"compose-proj_api:latest"}, + } + + // Créer un faux fichier compose pour que generateRunYAML puisse le relire (simplifié) + tempDir := t.TempDir() + composeContent := ` +services: + web: + build: ./web + ports: ["80:80"] + environment: { WEB_VAR: web_val } + depends_on: [api] + api: + build: ./api + environment: { API_VAR: api_val } +` + createTempFile(t, tempDir, "docker-compose.yml", composeContent) + spec.BuildConfig.ComposeFile = "docker-compose.yml" // Assurer que le chemin est relatif + + // Simuler un service (pas besoin de fetcher ici) + // On doit mettre le workDir pour que la relecture du compose file fonctionne + service, err := NewBuildService(tempDir, true, nil) + require.NoError(t, err) + // Simuler le buildDir qui aurait été créé par Build() + buildSpecificDir, err := os.MkdirTemp(tempDir, fmt.Sprintf("%s-%s-%d", spec.Name, spec.Version, time.Now().UnixNano())) + require.Nil(t, err) + createTempFile(t, buildSpecificDir, spec.BuildConfig.ComposeFile, composeContent) + + parsedComposeProject, err := LoadComposeFile([]byte(composeContent)) + require.Nil(t, err) + + runYAML, err := service.generateRunYAML(context.Background(), spec, result, runtimeEnv, finalImageTags, parsedComposeProject) + require.NoError(t, err) + require.NotNil(t, runYAML) + + require.Len(t, runYAML.Services, 2) + require.Contains(t, runYAML.Services, "web") + require.Contains(t, runYAML.Services, "api") + + webSvc := runYAML.Services["web"] + assert.Equal(t, "compose-proj_web.tar", webSvc.Image) // Référence locale + assert.Equal(t, "web_val", webSvc.Environment["WEB_VAR"]) + assert.Equal(t, "on", webSvc.Environment["GLOBAL"]) // Variable globale héritée + assert.Contains(t, webSvc.Ports, "80:80") + assert.Contains(t, webSvc.DependsOn, "api") + + apiSvc := runYAML.Services["api"] + assert.Equal(t, "compose-proj_api.tar", apiSvc.Image) + assert.Equal(t, "api_val", apiSvc.Environment["API_VAR"]) + assert.Equal(t, "on", apiSvc.Environment["GLOBAL"]) +} + +// Helper pour créer une archive tar.gz en mémoire (Alternative) +func createTarGz(t *testing.T, files map[string]string) []byte { + t.Helper() + + // 1. Créer l'archive TAR dans un buffer + var tarBuf bytes.Buffer + tw := tar.NewWriter(&tarBuf) + + for name, content := range files { + hdr := &tar.Header{ + Name: name, + Mode: 0644, + Size: int64(len(content)), + ModTime: time.Now(), // Ajouter un ModTime peut aider dans certains cas + } + err := tw.WriteHeader(hdr) + require.NoError(t, err, "Failed to write tar header for %s", name) + _, err = tw.Write([]byte(content)) + require.NoError(t, err, "Failed to write tar content for %s", name) + } + + // Fermer le writer TAR est crucial pour écrire les blocs de fin + err := tw.Close() + require.NoError(t, err, "Failed to close tar writer") + + // 2. Compresser le buffer TAR résultant en Gzip + var gzBuf bytes.Buffer + gzw := gzip.NewWriter(&gzBuf) + _, err = gzw.Write(tarBuf.Bytes()) + require.NoError(t, err, "Failed to write tar data to gzip writer") + + // Fermer le writer Gzip est crucial pour écrire le footer gzip + err = gzw.Close() + require.NoError(t, err, "Failed to close gzip writer") + + return gzBuf.Bytes() +} + +// Modifier aussi l'appel dans TestExtractTarGz pour être sûr +func TestExtractTarGz(t *testing.T) { + files := map[string]string{ + "file1.txt": "hello", + "dir1/file2.txt": "world", + "dir1/dir2/file3.txt": "nested", + } + tarGzData := createTarGz(t, files) + require.NotEmpty(t, tarGzData, "tar.gz data should not be empty") // Vérif simple + tempDir := t.TempDir() + + // Créer les lecteurs + gzr, err := gzip.NewReader(bytes.NewReader(tarGzData)) + require.NoError(t, err, "Failed to create gzip reader") + defer gzr.Close() // Bonne pratique de fermer le gzip reader + + tr := tar.NewReader(gzr) + + // Appeler extractTar (la fonction à tester) + err = extractTar(tr, tempDir) // Note: extractTar n'est pas défini dans build_test.go, il utilise celui de build.go + require.NoError(t, err, "extractTar failed") + + // Vérifier que les fichiers existent (assertions originales) + // ... (assertions inchangées) ... + content1, err := os.ReadFile(filepath.Join(tempDir, "file1.txt")) + require.NoError(t, err) + assert.Equal(t, "hello", string(content1)) + + content2, err := os.ReadFile(filepath.Join(tempDir, "dir1/file2.txt")) + require.NoError(t, err) + assert.Equal(t, "world", string(content2)) + + content3, err := os.ReadFile(filepath.Join(tempDir, "dir1/dir2/file3.txt")) + require.NoError(t, err) + assert.Equal(t, "nested", string(content3)) +} + +// --- Tests d'Intégration (nécessitent Docker) --- + +// Fonction pour skipper les tests d'intégration si Docker n'est pas dispo +func skipWithoutDocker(t *testing.T) { + t.Helper() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + t.Skipf("Skipping integration test: Docker client could not be initialized: %v", err) + } + _, err = cli.Ping(context.Background()) + if err != nil { + t.Skipf("Skipping integration test: Docker daemon is not responding: %v", err) + } + // Fermer le client ping pour ne pas laisser de connexions ouvertes + cli.Close() +} + +func TestIntegration_BuildSimpleDockerfile_LocalOutput(t *testing.T) { + // //go:build integration + skipWithoutDocker(t) + t.Parallel() // Peut être exécuté en parallèle avec d'autres tests d'intégration + + tempDir := t.TempDir() + mockFetcher := &MockSecretFetcher{Secrets: map[string]string{"my/secret": "verysecret"}} + + // Créer le service de build + service, err := NewBuildService(tempDir, false, mockFetcher) // Utiliser le système de fichiers + require.NoError(t, err) + + // Créer une codebase locale simple + codeDir := createTempDir(t, tempDir, "mycode") + dockerfileContent := ` +ARG BASE_IMG=alpine:latest +FROM ${BASE_IMG} +ARG my_arg=default_val +ENV MY_ARG_ENV=$my_arg +ENV MY_OTHER_ENV=static_value +COPY content.txt /app/ +WORKDIR /app +CMD ["cat", "content.txt"] +` + createTempFile(t, codeDir, "Dockerfile", dockerfileContent) + createTempFile(t, codeDir, "content.txt", "Hello Docker Build!") + + // Définir le BuildSpec + spec := &BuildSpec{ + Name: "integ-test-simple", + Version: fmt.Sprintf("0.1.0-%d", time.Now().Unix()), + Codebases: []CodebaseConfig{ + {Name: "main", SourceType: "local", Source: codeDir}, + }, + BuildConfig: BuildConfig{ + Dockerfile: "main/Dockerfile", // Chemin relatif au buildDir implicite + Tags: []string{fmt.Sprintf("integ-test-simple:%s", fmt.Sprintf("0.1.0-%d", time.Now().Unix()))}, + Args: map[string]string{"my_arg": "override_val"}, + OutputTarget: "local", // Tester la sortie locale + }, + RunConfigDef: RunConfigDef{Generate: true, ArtifactStorage: "local"}, + Env: map[string]string{"RUN_ENV": "run_value"}, + Secrets: []SecretSpec{{Name: "SECRET_ENV", Source: "my/secret"}}, + } + imageTag := spec.BuildConfig.Tags[0] // Tag principal + + // S'assurer que l'image est nettoyée à la fin + cli, _ := client.NewClientWithOpts(client.FromEnv) // Récupérer un client pour le cleanup + t.Cleanup(func() { + removeDockerImage(t, cli, imageTag) + cli.Close() + // Le tempDir est nettoyé automatiquement par Go + }) + + // Exécuter le build + ctx := context.Background() + result, err := service.Build(ctx, spec) + + // Assertions + require.NoError(t, err, "Build error message: %s", result.ErrorMessage) // Afficher le message d'erreur si le test échoue + require.True(t, result.Success, "Build should be successful") + require.NotEmpty(t, result.ImageIDs[spec.Name], "Image ID should not be empty") + assert.True(t, result.ImageSizes[spec.Name] > 0, "Image size should be positive") + assert.Contains(t, result.Logs, "Successfully built", "Build logs should indicate success") + assert.Contains(t, result.Logs, result.ImageIDs[spec.Name][:12], "Build logs should contain image ID") // Vérifier le short ID + + // Vérifier la sortie locale + require.NotEmpty(t, result.LocalImagePaths[spec.Name], "Local image path should be set") + localTarPath := result.LocalImagePaths[spec.Name] + // Vérifier que le chemin est relatif au workDir du service + assert.True(t, strings.HasPrefix(localTarPath, service.workDir), "Local path should be inside workDir") + _, err = os.Stat(localTarPath) + assert.NoError(t, err, "Local image tar file should exist at %s", localTarPath) + + // Vérifier le fichier run.yml + require.NotEmpty(t, result.RunConfigPath, "Run config path should be set") + assert.True(t, strings.HasPrefix(result.RunConfigPath, service.workDir), "Run config path should be inside workDir") + _, err = os.Stat(result.RunConfigPath) + assert.NoError(t, err, "run.yml file should exist at %s", result.RunConfigPath) + + // Lire et vérifier le contenu de run.yml + runData, err := os.ReadFile(result.RunConfigPath) + require.NoError(t, err) + var runContent RunYAML + err = yaml.Unmarshal(runData, &runContent) + require.NoError(t, err) + require.Contains(t, runContent.Services, spec.Name) + runService := runContent.Services[spec.Name] + assert.Equal(t, filepath.Base(localTarPath), runService.Image) // Doit référencer le fichier tar local + assert.Equal(t, "run_value", runService.Environment["RUN_ENV"]) + assert.Equal(t, "verysecret", runService.Environment["SECRET_ENV"]) // Secret injecté + + // Vérifier que l'image existe dans Docker (même si sortie locale, elle est buildée) + assert.True(t, dockerImageExists(t, cli, result.ImageIDs[spec.Name]), "Docker image should exist by ID") + assert.True(t, dockerImageExists(t, cli, imageTag), "Docker image should exist by Tag") + + // Optionnel: Charger l'image locale et vérifier son contenu + // _, err = service.dockerClient.ImageLoad(ctx, bytes.NewReader(localTarData)) ... +} + +func TestIntegration_BuildCompose_DockerOutput(t *testing.T) { + // //go:build integration + skipWithoutDocker(t) + t.Parallel() + + tempDir := t.TempDir() + service, err := NewBuildService(tempDir, false, nil) // Pas besoin de secrets ici + require.NoError(t, err) + + // Créer les codebases locales pour les services + webDir := createTempDir(t, tempDir, "frontend") + createTempFile(t, webDir, "Dockerfile", "FROM alpine:latest\nCMD echo web") + apiDir := createTempDir(t, tempDir, "backend") + createTempFile(t, apiDir, "Dockerfile", "FROM alpine:latest\nCMD echo api") + + // Créer le fichier docker-compose.yml + composeContent := ` +version: '3.8' +services: + web: + build: ./frontend # Chemin relatif au fichier compose + ports: ["8081:80"] + api: + build: ./backend + nginx: + image: nginx:alpine # Service sans build + ports: ["80:80"] +` + composePath := createTempFile(t, tempDir, "docker-compose.test.yml", composeContent) + + // Définir le BuildSpec + spec := &BuildSpec{ + Name: "integ-compose", + Version: fmt.Sprintf("ci-%d", time.Now().Unix()), + // Pas de Codebases ici car build.context est dans compose + BuildConfig: BuildConfig{ + ComposeFile: filepath.Base(composePath), // Doit être relatif au workDir du service + OutputTarget: "docker", // Sortie dans Docker daemon + }, + RunConfigDef: RunConfigDef{Generate: true, ArtifactStorage: "docker"}, + } + webImageTag := fmt.Sprintf("%s_web:latest", spec.Name) + apiImageTag := fmt.Sprintf("%s_api:latest", spec.Name) + nginxImage := "nginx:alpine" // Image qui sera pull + + cli, _ := client.NewClientWithOpts(client.FromEnv) + t.Cleanup(func() { + removeDockerImage(t, cli, webImageTag) + removeDockerImage(t, cli, apiImageTag) + // Ne pas supprimer nginx:alpine, c'est une image publique + cli.Close() + }) + + // Exécuter le build + ctx := context.Background() + result, err := service.Build(ctx, spec) + + // Assertions + require.NoError(t, err, "Build error message: %s", result.ErrorMessage) + require.True(t, result.Success, "Build should be successful") + + // Vérifier les résultats par service + require.Len(t, result.ServiceOutputs, 3, "Should have results for web, api, and nginx") // Nginx est inclus même si pas buildé + + require.Contains(t, result.ImageIDs, "web") + require.Contains(t, result.ImageIDs, "api") + assert.NotEmpty(t, result.ImageIDs["web"]) + assert.NotEmpty(t, result.ImageIDs["api"]) + // nginx n'a pas d'ID de build, mais l'image devrait exister + assert.Contains(t, result.Logs, "Pulling image 'nginx:alpine'...") + + assert.True(t, result.ImageSizes["web"] > 0) + assert.True(t, result.ImageSizes["api"] > 0) + + // Vérifier que les images existent dans Docker avec les tags par défaut + assert.True(t, dockerImageExists(t, cli, webImageTag), "Web image tag should exist") + assert.True(t, dockerImageExists(t, cli, apiImageTag), "API image tag should exist") + assert.True(t, dockerImageExists(t, cli, nginxImage), "Nginx image should exist (pulled)") + + // Vérifier run.yml + require.NotEmpty(t, result.RunConfigPath) + _, err = os.Stat(result.RunConfigPath) + require.NoError(t, err) + runData, err := os.ReadFile(result.RunConfigPath) + require.NoError(t, err) + var runContent RunYAML + err = yaml.Unmarshal(runData, &runContent) + require.NoError(t, err) + + require.Len(t, runContent.Services, 3) + assert.Equal(t, webImageTag, runContent.Services["web"].Image) + assert.Equal(t, apiImageTag, runContent.Services["api"].Image) + assert.Equal(t, nginxImage, runContent.Services["nginx"].Image) // Image directe du compose + assert.Contains(t, runContent.Services["web"].Ports, "8081:80") +} + +func TestIntegration_BuildGitRepo_GoGit(t *testing.T) { + // //go:build integration + skipWithoutDocker(t) + t.Parallel() + + tempDir := t.TempDir() + service, err := NewBuildService(tempDir, false, nil) + require.NoError(t, err) + + // Créer un repo Git local avec un Dockerfile + dockerfileContent := "FROM alpine:latest\nRUN echo 'Built from Git!' > /app/git.txt\nCMD cat /app/git.txt" + repoFiles := map[string]string{"Dockerfile": dockerfileContent, "README.md": "Test repo"} + repoURL, commitHash := setupLocalGitRepo(t, tempDir, repoFiles) + t.Logf("Created local git repo at %s with commit %s", repoURL, commitHash) + + // Définir le BuildSpec pour cloner et builder + spec := &BuildSpec{ + Name: "integ-git", + Version: fmt.Sprintf("git-%s", commitHash[:7]), + Codebases: []CodebaseConfig{ + { + Name: "app", + SourceType: "git", + Source: repoURL, // Utiliser l'URL file:// + Commit: commitHash, // Tester le checkout par commit + // TargetInHost: ".", // Mettre à la racine du buildDir + }, + }, + BuildConfig: BuildConfig{ + Dockerfile: "app/Dockerfile", // Doit trouver le Dockerfile dans le sous-dossier 'app' + Tags: []string{fmt.Sprintf("integ-git-test:%s", commitHash[:7])}, + OutputTarget: "docker", + }, + RunConfigDef: RunConfigDef{Generate: false}, // Pas besoin de run.yml ici + } + imageTag := spec.BuildConfig.Tags[0] + + cli, _ := client.NewClientWithOpts(client.FromEnv) + t.Cleanup(func() { + removeDockerImage(t, cli, imageTag) + cli.Close() + }) + + // Exécuter le build + ctx := context.Background() + result, err := service.Build(ctx, spec) + + // Assertions + require.NoError(t, err, "Build error message: %s", result.ErrorMessage) + require.True(t, result.Success, "Build should be successful") + require.NotEmpty(t, result.ImageIDs[spec.Name]) + assert.True(t, result.ImageSizes[spec.Name] > 0) + assert.Contains(t, result.Logs, "Cloning repository "+repoURL) + assert.Contains(t, result.Logs, "Checking out commit "+commitHash) + assert.Contains(t, result.Logs, "Successfully built") + + // Vérifier l'image dans Docker + assert.True(t, dockerImageExists(t, cli, imageTag), "Docker image from Git build should exist by tag") +} + +func TestIntegration_BuildWithResource(t *testing.T) { + // //go:build integration + skipWithoutDocker(t) + t.Parallel() + + // Créer un serveur HTTP mock pour la ressource + resourceContent := "This is the downloaded resource." + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/resource.txt" { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, resourceContent) + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer mockServer.Close() + + tempDir := t.TempDir() + service, err := NewBuildService(tempDir, false, nil) + require.NoError(t, err) + + // Codebase simple qui utilise la ressource + codeDir := createTempDir(t, tempDir, "res-app") + dockerfileContent := `FROM alpine:latest +COPY data/resource.txt /app/ +CMD cat /app/resource.txt +` + createTempFile(t, codeDir, "Dockerfile", dockerfileContent) + + spec := &BuildSpec{ + Name: "integ-res", + Version: "1.0", + Codebases: []CodebaseConfig{ + {Name: "app", SourceType: "local", Source: codeDir}, + }, + Resources: []ResourceConfig{ + { + URL: mockServer.URL + "/resource.txt", + TargetPath: "app/data/resource.txt", // Placer dans la codebase avant build + }, + }, + BuildConfig: BuildConfig{ + Dockerfile: "app/Dockerfile", + Tags: []string{"integ-res-test:latest"}, + OutputTarget: "docker", + }, + RunConfigDef: RunConfigDef{Generate: false}, + } + imageTag := spec.BuildConfig.Tags[0] + + cli, _ := client.NewClientWithOpts(client.FromEnv) + t.Cleanup(func() { + removeDockerImage(t, cli, imageTag) + cli.Close() + }) + + // Exécuter le build + ctx := context.Background() + result, err := service.Build(ctx, spec) + + // Assertions + require.NoError(t, err, "Build error message: %s", result.ErrorMessage) + require.True(t, result.Success) + require.NotEmpty(t, result.ImageIDs[spec.Name]) + + // Vérifier que la ressource a été téléchargée dans les logs + assert.Contains(t, result.Logs, "Downloading "+mockServer.URL+"/resource.txt") + // Vérifier que le fichier existe dans le buildDir temporaire avant le build (difficile à vérifier après cleanup) + // On se fie au succès du build Docker qui dépendait de la présence du fichier + + // Vérifier l'image + assert.True(t, dockerImageExists(t, cli, imageTag)) + + // Optionnel : vérifier le contenu de l'image + // output, err := service.ExecuteInContainer(ctx, result.ImageID, nil, nil) + // require.NoError(t, err) + // assert.Equal(t, resourceContent, output) +} + +// TODO: Ajouter TestIntegration_BuildWithSteps (plus complexe à mettre en place) diff --git a/bx/build/builder.go b/bx/build/builder.go index a3bda59..2a95b24 100644 --- a/bx/build/builder.go +++ b/bx/build/builder.go @@ -2,80 +2,161 @@ package build import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "context" - "encoding/base64" "encoding/json" "fmt" "io" - "io/ioutil" + "net/http" "os" "path/filepath" "strings" "sync" "time" + // Go-Git imports "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" - "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" + "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/joho/godotenv" // for the .env files loading + "github.com/moby/go-archive" + "github.com/moby/term" "gopkg.in/yaml.v3" - // Paquet pour B2 + // mod for B2 "github.com/Backblaze/blazer/b2" ) -// BuildSpec représente la configuration personnalisée de build +// --- Struct Definitions --- + +// BuildSpec is the specification structure parsing from the spec file +// This is the extended config for the build process type BuildSpec struct { - Name string `json:"name" yaml:"name"` - Version string `json:"version" yaml:"version"` - Codebases []CodebaseConfig `json:"codebases" yaml:"codebases"` - BuildConfig BuildConfig `json:"build_config" yaml:"build_config"` - Env map[string]string `json:"env" yaml:"env"` - Artifacts []string `json:"artifacts" yaml:"artifacts"` + Name string `json:"name" yaml:"name"` // The Name used for the service + Version string `json:"version" yaml:"version"` // The version of the software can use a semver specification + Codebases []CodebaseConfig `json:"codebases" yaml:"codebases"` // The list of the different codebases. It can be provided by git or local or tar/zip archive + Resources []ResourceConfig `json:"resources,omitempty" yaml:"resources,omitempty"` // A list of the resources to include in build process + BuildSteps []BuildStep `json:"build_steps,omitempty" yaml:"build_steps,omitempty"` // Specify the different build step. Useful for including a binary dependency in any codebase build + BuildConfig BuildConfig `json:"build_config" yaml:"build_config"` // The build Build configuration struct + Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` // Specify the Environment variables + EnvFiles []string `json:"env_files,omitempty" yaml:"env_files,omitempty"` // Used to load the Envs from the provided file path + Secrets []SecretSpec `json:"secrets,omitempty" yaml:"secrets,omitempty"` // Secrets specifications. Secrets is like env vars but it's provided by a specific service and encrypted/decrypted during the usage. Use this to pass very sensible information to your different services + RunConfigDef RunConfigDef `json:"run_config_def,omitempty" yaml:"run_config_def,omitempty"` // Configuration for the *.run.yml file. This file is used by the CLI to run your different services } -// CodebaseConfig représente une base de code source +// Representation of any codebase in the services type CodebaseConfig struct { - Name string `json:"name" yaml:"name"` - SourceType string `json:"source_type" yaml:"source_type"` // git, local, archive, buffer - Source string `json:"source" yaml:"source"` // URL, chemin local - Branch string `json:"branch" yaml:"branch"` - Commit string `json:"commit" yaml:"commit"` - Path string `json:"path" yaml:"path"` // Sous-répertoire si nécessaire - Content []byte `json:"-" yaml:"-"` // Contenu en mémoire pour le type buffer + Name string `json:"name" yaml:"name"` // Specify the name of the codebase + SourceType string `json:"source_type" yaml:"source_type"` // git, local, archive, buffer + Source string `json:"source" yaml:"source"` // URL, local path + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` // The git branch to build + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` // The specific commit to consider during the codebase pulling if the source is git + Path string `json:"path,omitempty" yaml:"path,omitempty"` // The path of the codebase in the local dir + Content []byte `json:"-" yaml:"-"` // The memory content if the source type is buffer + BuildOnly bool `json:"build_only,omitempty" yaml:"build_only,omitempty"` // If specified the codebase is only builded + TargetInHost string `json:"target_in_host,omitempty" yaml:"target_in_host,omitempty"` // Path to put the codebase in the host dir +} + +// ResourceConfig is resource representation to download during the build +type ResourceConfig struct { + URL string `json:"url" yaml:"url"` // The resource URL + TargetPath string `json:"target_path" yaml:"target_path"` // relative path destination in the build dir + Extract bool `json:"extract,omitempty" yaml:"extract,omitempty"` // Extract the archive (tar, tgz, zip) +} + +// BuildStep is a build sequenced step, potentially with dependencies +type BuildStep struct { + Name string `json:"name" yaml:"name"` // The step name + CodebaseName string `json:"codebase_name" yaml:"codebase_name"` // References a codebase name to use for this step + OutputsBinaryPath string `json:"outputs_binary_path,omitempty" yaml:"outputs_binary_path,omitempty"` // Path in the *container* of the binary to extract + UseBinaryFromStep string `json:"use_binary_from_step,omitempty" yaml:"use_binary_from_step,omitempty"` // The step in which the binary will be used + BinaryTargetPath string `json:"binary_target_path,omitempty" yaml:"binary_target_path,omitempty"` // The path to put the binary during the specific step } -// BuildConfig représente la configuration de build Docker +// BuildConfig is a Docker build config spec extended type BuildConfig struct { - BaseImage string `json:"base_image" yaml:"base_image"` - Dockerfile string `json:"dockerfile" yaml:"dockerfile"` - ComposeFile string `json:"compose_file" yaml:"compose_file"` - Target string `json:"target" yaml:"target"` - Args map[string]string `json:"args" yaml:"args"` - Tags []string `json:"tags" yaml:"tags"` - Platforms []string `json:"platforms" yaml:"platforms"` - NoCache bool `json:"no_cache" yaml:"no_cache"` + BaseImage string `json:"base_image,omitempty" yaml:"base_image,omitempty"` // The base image to use + Dockerfile string `json:"dockerfile,omitempty" yaml:"dockerfile,omitempty"` // relative path of the Dockerfile or the inline content + ComposeFile string `json:"compose_file,omitempty" yaml:"compose_file,omitempty"` // the relative compose file path + Target string `json:"target,omitempty" yaml:"target,omitempty"` + Args map[string]string `json:"args,omitempty" yaml:"args,omitempty"` // Ens vars to inject in the build config + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` // Tags for the finale docker image (or the principal image in case of compose) + Platforms []string `json:"platforms,omitempty" yaml:"platforms,omitempty"` // cross-platform support (experimental) + NoCache bool `json:"no_cache,omitempty" yaml:"no_cache,omitempty"` // Specify if the cache will be used between the build + OutputTarget string `json:"output_target" yaml:"output_target"` // The storage target "b2", "local", "docker" (by default) + LocalPath string `json:"local_path,omitempty" yaml:"local_path,omitempty"` // Output path if OutputTarget="local" + Pull bool `json:"pull,omitempty" yaml:"pull,omitempty"` // Trying to pull the based image + BuildKit bool `json:"buildkit,omitempty" yaml:"buildkit,omitempty"` // Use BuildKit (if available) +} + +// SecretSpec define the way to fetch the secrets +type SecretSpec struct { + Name string `json:"name" yaml:"name"` // The name of the env var that will receive the secret + Source string `json:"source" yaml:"source"` // The service ID for this secret + InjectMethod string `json:"inject_method" yaml:"inject_method"` // "env" (default), can be file later +} + +// RunConfigDef define the parameters for the *.run.yml generation +type RunConfigDef struct { + Generate bool `json:"generate" yaml:"generate"` // Is the file will be generated ? + ArtifactStorage string `json:"artifact_storage" yaml:"artifact_storage"` // "docker" (use the tags), "local" (referencing .tar) + Commands []string `json:"commands,omitempty" yaml:"commands,omitempty"` // The default commands (overriding if needed) + // Some other options can be added after... +} + +// RunService is any service representation in the *.run.yml +type RunService struct { + Image string `yaml:"image"` // The name of the tar local image + Command []string `yaml:"command,omitempty"` // The command to exec + Entrypoint []string `yaml:"entrypoint,omitempty"` // The entry point + Environment map[string]string `yaml:"environment,omitempty"` // Environment variables (include secrets) + Ports []string `yaml:"ports,omitempty"` // Format "host:container" + Volumes []string `yaml:"volumes,omitempty"` // Format "host:container" ou "named:container" + Restart string `yaml:"restart,omitempty"` // Reboot politic (e.g., "always", "on-failure") + DependsOn []string `yaml:"depends_on,omitempty"` // The depending services + // Some other fields can be added later... +} + +// RunYAML is the struct of the *.run.yml output file. This file is generated after a build and is used by the bx CLI to run your artifact +type RunYAML struct { + Version string `yaml:"version"` // The file version format + Services map[string]RunService `yaml:"services"` + // potentially other sections for volumes, networks, etc. } -// BuildResult contient les résultats du processus de build +// BuildResult is the struct representing a build result of each service type BuildResult struct { - Success bool `json:"success"` - ImageID string `json:"image_id"` - ImageSize int64 `json:"image_size"` - Artifacts map[string][]byte `json:"artifacts"` - BuildTime float64 `json:"build_time"` - ErrorMessage string `json:"error_message,omitempty"` - Logs string `json:"logs"` - B2ObjectNames []string `json:"b2_object_names,omitempty"` + Success bool `json:"success"` + ImageID string `json:"image_id,omitempty"` // The docker image ID (if applicable) + ImageIDs map[string]string `json:"image_ids,omitempty"` // Each service IDS (if compose) + ImageSize int64 `json:"image_size,omitempty"` // The main docker image size + ImageSizes map[string]int64 `json:"image_sizes,omitempty"` // Image size by service + Artifacts map[string][]byte `json:"-"` // Memory artefact + BuildTime float64 `json:"build_time"` // Total Build time + ErrorMessage string `json:"error_message,omitempty"` // Build error message + Logs string `json:"logs"` // Build logs + B2ObjectNames []string `json:"b2_object_names,omitempty"` // For OutputTarget="b2" + LocalImagePaths map[string]string `json:"local_image_paths,omitempty"` // For OutputTarget="local" + RunConfigPath string `json:"run_config_path,omitempty"` // Path to the generated *.run.yml file + ServiceOutputs map[string]ServiceOutput `json:"service_outputs,omitempty"` // Specific information generated by service +} + +// ServiceOutput is the specific information for each builded service (e.g., image ID) +type ServiceOutput struct { + ImageID string `json:"image_id"` + ImageSize int64 `json:"image_size"` + Logs string `json:"logs"` } -// B2Config contient les informations de configuration pour le stockage B2 +// B2Config is the b2 storage information struct type B2Config struct { AccountID string `json:"account_id" yaml:"account_id"` ApplicationKey string `json:"application_key" yaml:"application_key"` @@ -83,230 +164,951 @@ type B2Config struct { BasePath string `json:"base_path" yaml:"base_path"` } -// BuildService est le service principal qui gère les builds +// The Main service to manage each build type BuildService struct { - dockerClient *client.Client - workDir string - b2Config *B2Config - mutex sync.Mutex - inMemory bool // Si true, fonctionne principalement en mémoire + dockerClient *client.Client + workDir string + b2Config *B2Config + mutex sync.Mutex + inMemory bool // if true minimizing the system disk usage + secretFetcher SecretFetcher // Interface for secrets fetching +} + +type ComposeProject struct { + Version string `yaml:"version,omitempty"` + Services map[string]ComposeService `yaml:"services"` + Name string + Volumes map[string]interface{} `yaml:"volumes,omitempty"` + Networks map[string]interface{} `yaml:"networks,omitempty"` +} + +// A representation of a compose service (simplified) +type ComposeService struct { + Image string `yaml:"image,omitempty"` + Build *ComposeBuild `yaml:"build,omitempty"` + Command []string `yaml:"command,omitempty"` + Entrypoint []string `yaml:"entrypoint,omitempty"` + Environment map[string]*string `yaml:"environment,omitempty"` + Ports []string `yaml:"ports,omitempty"` + Volumes []string `yaml:"volumes,omitempty"` + DependsOn []string `yaml:"depends_on,omitempty"` + Restart string `yaml:"restart,omitempty"` + HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Expose []string `yaml:"expose,omitempty"` + StopGracePeriod string `yaml:"stop_grace_period,omitempty"` +} + +type ComposeBuild struct { + Context string + Dockerfile string + Args map[string]*string + Target string + CacheFrom []string `yaml:"cache_from,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Network string `yaml:"network,omitempty"` +} + +// UnmarshalYAML handle the case which `build: ./context` and `build: {context: ...}` +func (cb *ComposeBuild) UnmarshalYAML(value *yaml.Node) error { + if value.Kind == yaml.ScalarNode { // Case build: ./context + cb.Context = value.Value + return nil + } + // Case build: { ... } (map) + // use a temp type to avoid the infinite recursion + type ComposeBuildMap struct { + Context string `yaml:"context,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty"` + Args map[string]*string `yaml:"args,omitempty"` + Target string `yaml:"target,omitempty"` + CacheFrom []string `yaml:"cache_from,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Network string `yaml:"network,omitempty"` + } + var temp ComposeBuildMap + if err := value.Decode(&temp); err != nil { + return err + } + cb.Context = temp.Context + cb.Dockerfile = temp.Dockerfile + cb.Args = temp.Args + cb.Target = temp.Target + cb.CacheFrom = temp.CacheFrom + cb.Labels = temp.Labels + cb.Network = temp.Network + + // Apply the default if context is empty but build is a non empty map + if cb.Context == "" && !value.IsZero() && value.Kind == yaml.MappingNode { + cb.Context = "." + } + return nil +} + +// This is a healthcheck simplified struct +type HealthCheck struct { + Test []string `yaml:"test,omitempty"` + Interval string `yaml:"interval,omitempty"` + Timeout string `yaml:"timeout,omitempty"` + Retries *int `yaml:"retries,omitempty"` + StartPeriod string `yaml:"start_period,omitempty"` +} + +// Interface for an extern secrets service provider +type SecretFetcher interface { + GetSecret(ctx context.Context, source string) (string, error) // Must return the secret value } -// NewBuildService crée une nouvelle instance du service de build -func NewBuildService(workDir string, inMemory bool) (*BuildService, error) { +// --- Service Initialization --- + +// Create a new instance of the build service +func NewBuildService(workDir string, inMemory bool, secretFetcher SecretFetcher) (*BuildService, error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - return nil, fmt.Errorf("erreur lors de la création du client Docker: %w", err) + return nil, fmt.Errorf("error during the Docker client initialization: %w", err) } - // Création du répertoire de travail si nécessaire et si pas en mode mémoire - if !inMemory && workDir != "" { + // Creating the working directory + effectiveWorkDir := workDir + if inMemory && workDir == "" { + // Memory mode creating the working temp dir + tmpDir, err := os.MkdirTemp("", "buildservice-work-") + if err != nil { + return nil, fmt.Errorf("failed to create the temp working dir: %w", err) + } + effectiveWorkDir = tmpDir + // TODO: Assuming that the program delete this temp dir + } else if !inMemory && workDir != "" { if err := os.MkdirAll(workDir, 0755); err != nil { - return nil, fmt.Errorf("impossible de créer le répertoire %s: %w", workDir, err) + return nil, fmt.Errorf("working dir creation failed %s: %w", workDir, err) } } return &BuildService{ - dockerClient: cli, - workDir: workDir, - inMemory: inMemory, - mutex: sync.Mutex{}, + dockerClient: cli, + workDir: effectiveWorkDir, + inMemory: inMemory, + secretFetcher: secretFetcher, // Inject the secret fetcher + mutex: sync.Mutex{}, }, nil } -// SetB2Config configure les paramètres pour le stockage B2 +func (s *BuildService) Cleanup() error { + if err := os.RemoveAll(s.workDir); err != nil { + return fmt.Errorf("failed to clean the working dir: %s %w", s.workDir, err) + } + + return nil +} + +// SetB2Config configure the B2 configuration func (s *BuildService) SetB2Config(config *B2Config) { s.b2Config = config } -// LoadBuildSpecFromFile charge une spécification de build depuis un fichier +// --- Configuration Loading --- + +// Load the build config from a file func LoadBuildSpecFromFile(filename string) (*BuildSpec, error) { data, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("impossible de lire le fichier de spécification: %w", err) + return nil, fmt.Errorf("cannot read the build file specification '%s': %w", filename, err) } - return LoadBuildSpecFromBytes(data, filepath.Ext(filename)) } -// LoadBuildSpecFromBytes charge une spécification de build depuis des données en mémoire +// Load the build config from byte array func LoadBuildSpecFromBytes(data []byte, format string) (*BuildSpec, error) { var spec BuildSpec var err error + // Set defaults + spec.BuildConfig.OutputTarget = "docker" // Default output target + spec.RunConfigDef.Generate = true // Default to generating run config + spec.RunConfigDef.ArtifactStorage = "docker" // Default artifact storage for run config + if format == ".json" { err = json.Unmarshal(data, &spec) } else if format == ".yaml" || format == ".yml" { err = yaml.Unmarshal(data, &spec) } else { - return nil, fmt.Errorf("format non pris en charge: %s", format) + // Try YAML decoding by default if format is unknown or missing + err = yaml.Unmarshal(data, &spec) + if err != nil { + // If YAML fails, try JSON as a fallback + errJson := json.Unmarshal(data, &spec) + if errJson != nil { + return nil, fmt.Errorf("invalid format. YAML error: %v, JSON error: %v", err, errJson) + } + err = nil // JSON succeeded + } } if err != nil { - return nil, fmt.Errorf("impossible de parser la spécification: %w", err) + return nil, fmt.Errorf("specification parsing failed (format: %s): %w", format, err) + } + + // Basic Validation + if spec.Name == "" || spec.Version == "" { + return nil, fmt.Errorf("the fields 'name' and 'version' are required in the specification") + } + if len(spec.Codebases) == 0 && len(spec.BuildSteps) == 0 && spec.BuildConfig.Dockerfile == "" && spec.BuildConfig.ComposeFile == "" { + return nil, fmt.Errorf("no codebase, build_step, dockerfile or compose_file specified") + } + if spec.BuildConfig.Dockerfile != "" && spec.BuildConfig.ComposeFile != "" { + return nil, fmt.Errorf("don't specify 'dockerfile' et 'compose_file' in the build_config") } return &spec, nil } -// Build exécute un build selon la spécification donnée +// parse a compose file +func LoadComposeFile(data []byte) (*ComposeProject, error) { + var project ComposeProject + err := yaml.Unmarshal(data, &project) + if err != nil { + return nil, fmt.Errorf("error during the compose YAML file parsing: %w", err) + } + if len(project.Services) == 0 { + return nil, fmt.Errorf("no service section found in the compose file config") + } + // Initializing the maps/slices nil to avoid the nil pointer panics + for _, service := range project.Services { + if service.Environment == nil { + service.Environment = make(map[string]*string) + } + if service.Build != nil && service.Build.Args == nil { + service.Build.Args = make(map[string]*string) + } + // TODO: do this for other map slice... + } + return &project, nil +} + +func (s *BuildService) GetSecret(ctx context.Context, source string) (string, error) { + s.mutex.Lock() + fetcher := s.secretFetcher + defer s.mutex.Unlock() + + if fetcher == nil { + // Using the default DummySecretFetcher if no fetcher is initialized + fetcher = &DummySecretFetcher{} + } + return fetcher.GetSecret(ctx, source) +} + +// --- Core Build Logic --- + +// Running the build based on the provided spec func (s *BuildService) Build(ctx context.Context, spec *BuildSpec) (*BuildResult, error) { s.mutex.Lock() defer s.mutex.Unlock() startTime := time.Now() result := &BuildResult{ - Artifacts: make(map[string][]byte), - Logs: "", + Artifacts: make(map[string][]byte), // Legacy, might remove + Logs: "", + ImageIDs: make(map[string]string), + ImageSizes: make(map[string]int64), + LocalImagePaths: make(map[string]string), + ServiceOutputs: make(map[string]ServiceOutput), } + var overallLogs strings.Builder // Collect logs from all steps - var buildDir string - var cleanupFunc func() + // --- 1. Setup Build Environment --- + buildID := fmt.Sprintf("%s-%s-%d", spec.Name, spec.Version, time.Now().UnixNano()) + buildDir := filepath.Join(s.workDir, buildID) // Main directory for this build - if s.inMemory { - // Utilisation d'un répertoire temporaire pour les opérations nécessitant des fichiers - tmpDir, err := os.MkdirTemp("", fmt.Sprintf("docker-build-%s-%s-", spec.Name, spec.Version)) + if err := os.MkdirAll(buildDir, 0755); err != nil { + result.Success = false + result.ErrorMessage = fmt.Sprintf("cannot create the build dir '%s': %v", buildDir, err) + return result, fmt.Errorf("error during the run: \n %s", result.ErrorMessage) + } + // Cleanup build directory unless OutputTarget is local and no path is specified + shouldCleanup := !(spec.BuildConfig.OutputTarget == "local" && spec.BuildConfig.LocalPath == "") + if shouldCleanup { + defer func() { + // Add some robustness: Check if buildDir still exists + if _, err := os.Stat(buildDir); err == nil || !os.IsNotExist(err) { + os.RemoveAll(buildDir) + } + }() + } + overallLogs.WriteString(fmt.Sprintf("Using build directory: %s\n", buildDir)) + + // --- 2. Load Environment Variables --- + mergedEnv := make(map[string]string) + // Load from EnvFiles first + for _, envFile := range spec.EnvFiles { + // Assume relative path to buildDir or potentially absolute path? Let's try relative first. + envFilePath := filepath.Join(buildDir, envFile) // Or maybe relative to spec file location? Needs clarification. + if _, err := os.Stat(envFilePath); os.IsNotExist(err) { + // If relative to buildDir doesn't exist, try absolute + envFilePath = envFile + } + envMap, err := godotenv.Read(envFilePath) if err != nil { - return result, fmt.Errorf("impossible de créer un répertoire temporaire: %w", err) + overallLogs.WriteString(fmt.Sprintf("Warning: cannot read env file '%s': %v\n", envFile, err)) + } else { + for k, v := range envMap { + if _, exists := mergedEnv[k]; !exists { // Avoid overriding already set vars from earlier files + mergedEnv[k] = v + } + } } - buildDir = tmpDir - cleanupFunc = func() { os.RemoveAll(tmpDir) } - } else { - // Créer un répertoire de build unique dans le workDir - buildID := fmt.Sprintf("%s-%s-%d", spec.Name, spec.Version, time.Now().Unix()) - buildDir = filepath.Join(s.workDir, buildID) - if err := os.MkdirAll(buildDir, 0755); err != nil { - return result, fmt.Errorf("impossible de créer le répertoire de build: %w", err) + } + // Override with spec.Env + for k, v := range spec.Env { + mergedEnv[k] = v + } + overallLogs.WriteString(fmt.Sprintf("Loaded %d environment variables\n", len(mergedEnv))) + + // --- 3. Fetch Secrets (Placeholder) --- + runtimeSecrets := make(map[string]string) // Secrets for runtime (.run.yml) + if s.secretFetcher != nil && len(spec.Secrets) > 0 { + overallLogs.WriteString("Fetching secrets...\n") + for _, secretSpec := range spec.Secrets { + if secretSpec.InjectMethod == "" || secretSpec.InjectMethod == "env" { + secretValue, err := s.secretFetcher.GetSecret(ctx, secretSpec.Source) + if err != nil { + errMsg := fmt.Sprintf("error during the secret creation '%s' (source: %s): %v", secretSpec.Name, secretSpec.Source, err) + overallLogs.WriteString(errMsg + "\n") + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + runtimeSecrets[secretSpec.Name] = secretValue + overallLogs.WriteString(fmt.Sprintf("Secret '%s' fetched successfully.\n", secretSpec.Name)) + } else { + overallLogs.WriteString(fmt.Sprintf("Warning: Secret injection method '%s' for '%s' not yet supported.\n", secretSpec.InjectMethod, secretSpec.Name)) + } + } + } + + // Combine regular envs and secret envs for runtime config + finalRuntimeEnv := make(map[string]string) + for k, v := range mergedEnv { + finalRuntimeEnv[k] = v + } + for k, v := range runtimeSecrets { + finalRuntimeEnv[k] = v // Secrets override regular env if names clash + } + + // --- 4. Download Resources --- + overallLogs.WriteString("Downloading resources...\n") + for _, res := range spec.Resources { + overallLogs.WriteString(fmt.Sprintf("Downloading %s to %s...\n", res.URL, res.TargetPath)) + targetFullPath := filepath.Join(buildDir, res.TargetPath) + targetDir := filepath.Dir(targetFullPath) + if err := os.MkdirAll(targetDir, 0755); err != nil { + errMsg := fmt.Sprintf("error during the resource target directory creation '%s': %v", targetFullPath, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + err := s.downloadFile(ctx, res.URL, targetFullPath) + if err != nil { + errMsg := fmt.Sprintf("error during the resource downloading '%s': %v", res.URL, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + if res.Extract { + overallLogs.WriteString(fmt.Sprintf("Extracting %s...\n", targetFullPath)) + // Extract needs to place files inside targetDir, not create a new subdir named after the archive + err := s.extractArchive(targetFullPath, targetDir) + if err != nil { + errMsg := fmt.Sprintf("error during the archive extraction '%s': %v", targetFullPath, err) + // Log warning but continue? Or fail? Let's fail for now. + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + // Optionally remove the archive after extraction + os.Remove(targetFullPath) + overallLogs.WriteString(fmt.Sprintf("Extracted %s successfully.\n", res.TargetPath)) } - cleanupFunc = func() { os.RemoveAll(buildDir) } } - defer cleanupFunc() - // Traiter les codebases + // --- 5. Prepare Codebases --- + overallLogs.WriteString("Fetching codebases...\n") + codebaseMap := make(map[string]CodebaseConfig) // For easy lookup by name for _, codebase := range spec.Codebases { - destDir := filepath.Join(buildDir, codebase.Name) + codebaseMap[codebase.Name] = codebase + var destDir string + // If TargetInHost is specified, place it there relative to buildDir + if codebase.TargetInHost != "" { + destDir = filepath.Join(buildDir, codebase.TargetInHost) + } else { + // Default: place it in a subdirectory named after the codebase + destDir = filepath.Join(buildDir, codebase.Name) + } + + overallLogs.WriteString(fmt.Sprintf("Fetching codebase '%s' (%s: %s) into %s\n", codebase.Name, codebase.SourceType, codebase.Source, destDir)) if err := s.fetchCodebase(ctx, codebase, destDir); err != nil { + errMsg := fmt.Sprintf("error during the codebase fetching '%s': %v", codebase.Name, err) result.Success = false - result.ErrorMessage = fmt.Sprintf("erreur lors de la récupération de la codebase %s: %s", codebase.Name, err) - return result, err + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) } } - // Préparer le Dockerfile - dockerfilePath := "" - if spec.BuildConfig.Dockerfile != "" { - dockerfilePath = filepath.Join(buildDir, "Dockerfile") - if err := os.WriteFile(dockerfilePath, []byte(spec.BuildConfig.Dockerfile), 0644); err != nil { + // --- 6. Execute Build Steps (Sequential Build & Binary Handling) --- + extractedBinaries := make(map[string][]byte) // Map step name -> binary data + overallLogs.WriteString("Executing build steps...\n") + for _, step := range spec.BuildSteps { + overallLogs.WriteString(fmt.Sprintf("--- Build Step: %s ---\n", step.Name)) + cb, ok := codebaseMap[step.CodebaseName] + if !ok { + errMsg := fmt.Sprintf("build step '%s' referencing a non existent codebase: '%s'", step.Name, step.CodebaseName) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + stepBuildDir := filepath.Join(buildDir, cb.Name) // Assume codebase is in its named dir + + // Inject binary from previous step if needed + if step.UseBinaryFromStep != "" { + binaryData, exists := extractedBinaries[step.UseBinaryFromStep] + if !exists { + errMsg := fmt.Sprintf("build step '%s' require a binary for the step '%s', but it's not found", step.Name, step.UseBinaryFromStep) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + if step.BinaryTargetPath == "" { + errMsg := fmt.Sprintf("build step '%s' uses a 'binary_target_path' not defined", step.Name) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + targetBinaryPath := filepath.Join(stepBuildDir, step.BinaryTargetPath) + targetBinaryDir := filepath.Dir(targetBinaryPath) + overallLogs.WriteString(fmt.Sprintf("Injecting binary from step '%s' to '%s'\n", step.UseBinaryFromStep, targetBinaryPath)) + if err := os.MkdirAll(targetBinaryDir, 0755); err != nil { + errMsg := fmt.Sprintf("error during the repertory '%s' creation for the injected binary: %v", targetBinaryDir, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + if err := os.WriteFile(targetBinaryPath, binaryData, 0755); err != nil { // Make executable + errMsg := fmt.Sprintf("error during the binary writing '%s': %v", targetBinaryPath, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + } + + // Build this step's codebase (assuming it has a Dockerfile) + // We need a way to find the Dockerfile for this specific step/codebase + stepDockerfilePath := filepath.Join(stepBuildDir, "Dockerfile") // Default assumption + // Allow overriding Dockerfile path via CodebaseConfig or BuildStep? For now, default. + if _, err := os.Stat(stepDockerfilePath); os.IsNotExist(err) { + errMsg := fmt.Sprintf("No Dockerfile founded '%s' in the build step '%s' (waiting path: %s)", cb.Name, step.Name, stepDockerfilePath) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + // Create a temporary BuildSpec for this step + stepSpec := &BuildSpec{ + Name: fmt.Sprintf("%s-%s-step-%s", spec.Name, spec.Version, step.Name), + Version: "latest", + BuildConfig: BuildConfig{ + // Use build args from the main spec? Or step-specific? Let's use main spec for now. + Args: spec.BuildConfig.Args, + NoCache: spec.BuildConfig.NoCache, + Tags: []string{fmt.Sprintf("%s-%s-step-%s:latest", spec.Name, spec.Version, step.Name)}, // Temporary tag + Pull: spec.BuildConfig.Pull, + }, + } + + // Build the image for the step + stepImageID, stepLogs, err := s.buildSingleImage(ctx, stepBuildDir, stepDockerfilePath, stepSpec) + overallLogs.WriteString(fmt.Sprintf("Logs for step %s:\n%s\n", step.Name, stepLogs)) + if err != nil { + errMsg := fmt.Sprintf("error during the step build '%s': %v", step.Name, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + overallLogs.WriteString(fmt.Sprintf("Step '%s' built successfully, ImageID: %s\n", step.Name, stepImageID)) + + // Extract binary if needed + if step.OutputsBinaryPath != "" { + overallLogs.WriteString(fmt.Sprintf("Extracting binary '%s' from step '%s' image %s\n", step.OutputsBinaryPath, step.Name, stepImageID)) + binaryData, err := s.extractFromContainer(ctx, stepImageID, step.OutputsBinaryPath) + if err != nil { + errMsg := fmt.Sprintf("erro during the extraction of the binary '%s' in the step '%s': %v", step.OutputsBinaryPath, step.Name, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + extractedBinaries[step.Name] = binaryData + overallLogs.WriteString(fmt.Sprintf("Binary extracted successfully (%d bytes).\n", len(binaryData))) + } + overallLogs.WriteString(fmt.Sprintf("--- End Build Step: %s ---\n", step.Name)) + } // End of build steps loop + + // --- 7. Main Build Execution --- + overallLogs.WriteString("--- Starting Main Build ---\n") + + if spec.BuildConfig.ComposeFile != "" { + // --- 7a. Build using Docker Compose --- + overallLogs.WriteString(fmt.Sprintf("Building using Compose file: %s\n", spec.BuildConfig.ComposeFile)) + composeFilePath := filepath.Join(buildDir, spec.BuildConfig.ComposeFile) + composeData, err := os.ReadFile(composeFilePath) + if err != nil { + errMsg := fmt.Sprintf("error during the compose file reading '%s': %v", composeFilePath, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + // Use the provided LoadComposeFile function (assuming it's adapted for compose-go v2) + composeProject, err := LoadComposeFile(composeData) + if err != nil { + errMsg := fmt.Sprintf("error during the compose file parsing '%s': %v", spec.BuildConfig.ComposeFile, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + buildErrs := s.buildComposeProject(ctx, buildDir, composeProject, spec, result, &overallLogs) + if len(buildErrs) > 0 { + errMsg := fmt.Sprintf("errors during the compose project building: %v", buildErrs) result.Success = false - result.ErrorMessage = "erreur lors de la création du Dockerfile: " + err.Error() - return result, err + result.ErrorMessage = strings.Join(buildErrs, "; ") + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) } + // Note: ImageID in result might remain empty if compose file only defines services with existing images + overallLogs.WriteString("Compose project built successfully.\n") + } else { - // Chercher un Dockerfile existant - for _, codebase := range spec.Codebases { - dfPath := filepath.Join(buildDir, codebase.Name, "Dockerfile") + // --- 7b. Build using Dockerfile --- + dockerfilePath := "" + buildContextDir := buildDir // Default context is the root build directory + + if spec.BuildConfig.Dockerfile != "" { + // Check if Dockerfile content is inline or a path + if strings.Contains(spec.BuildConfig.Dockerfile, "\n") { + // Inline Dockerfile content + dockerfilePath = filepath.Join(buildDir, "Dockerfile.inline") + if err := os.WriteFile(dockerfilePath, []byte(spec.BuildConfig.Dockerfile), 0644); err != nil { + errMsg := fmt.Sprintf("error during the inline Dockerfile creation: %v", err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + overallLogs.WriteString("Using inline Dockerfile.\n") + } else { + // Path to Dockerfile relative to buildDir + dockerfilePath = filepath.Join(buildDir, spec.BuildConfig.Dockerfile) + // The build context might need adjustment if the Dockerfile is not at the root + buildContextDir = filepath.Dir(dockerfilePath) + overallLogs.WriteString(fmt.Sprintf("Using Dockerfile at path: %s\n", spec.BuildConfig.Dockerfile)) + } + } else { + // Auto-detect Dockerfile (simple case: look for Dockerfile at the root) + dfPath := filepath.Join(buildDir, "Dockerfile") if _, err := os.Stat(dfPath); err == nil { dockerfilePath = dfPath - break + buildContextDir = buildDir + overallLogs.WriteString("Auto-detected Dockerfile at build root.\n") + } else { + // Try finding in the first codebase dir (legacy behavior, might need refinement) + if len(spec.Codebases) > 0 { + firstCodebaseDir := filepath.Join(buildDir, spec.Codebases[0].Name) + dfPath = filepath.Join(firstCodebaseDir, "Dockerfile") + if _, err := os.Stat(dfPath); err == nil { + dockerfilePath = dfPath + buildContextDir = firstCodebaseDir // Context is the codebase dir + overallLogs.WriteString(fmt.Sprintf("Auto-detected Dockerfile in first codebase: %s\n", spec.Codebases[0].Name)) + } + } } } + if dockerfilePath == "" { + errMsg := "not found/provided Dockerfile for the build" result.Success = false - result.ErrorMessage = "aucun Dockerfile trouvé" - return result, fmt.Errorf("aucun Dockerfile trouvé") + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) } - } - // Construction de l'image Docker - imageID, logs, err := s.buildWithDocker(ctx, buildDir, dockerfilePath, spec) - result.Logs = logs - if err != nil { - result.Success = false - result.ErrorMessage = "erreur lors du build Docker: " + err.Error() - return result, err + // Perform the build for the single Dockerfile + imageID, logs, err := s.buildSingleImage(ctx, buildContextDir, dockerfilePath, spec) + overallLogs.WriteString(fmt.Sprintf("Dockerfile Build Logs:\n%s\n", logs)) + if err != nil { + errMsg := fmt.Sprintf("erreur lors du build Docker: %v", err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + // Store result for the single image build + result.ImageID = imageID + imageSize, err := s.getImageSize(ctx, imageID) + if err == nil { + result.ImageSize = imageSize + } else { + overallLogs.WriteString(fmt.Sprintf("Warning: could not get size for image %s: %v\n", imageID, err)) + } + // Add to ServiceOutputs as a pseudo-service if needed for consistency + mainServiceName := spec.Name // Use build name as service name + result.ServiceOutputs[mainServiceName] = ServiceOutput{ + ImageID: imageID, + ImageSize: imageSize, + Logs: logs, + } + result.ImageIDs[mainServiceName] = imageID + result.ImageSizes[mainServiceName] = imageSize + + overallLogs.WriteString(fmt.Sprintf("Dockerfile build successful. ImageID: %s, Size: %d\n", imageID, imageSize)) } - result.ImageID = imageID - // Récupérer la taille de l'image - imageSize, err := s.getImageSize(ctx, imageID) - if err == nil { - result.ImageSize = imageSize + // --- 8. Handle Build Outputs (Save/Upload Images) --- + outputBasePath := buildDir // Default base for local output + if spec.BuildConfig.OutputTarget == "local" && spec.BuildConfig.LocalPath != "" { + outputBasePath = spec.BuildConfig.LocalPath + if err := os.MkdirAll(outputBasePath, 0755); err != nil { + errMsg := fmt.Sprintf("cannot create the output base directory '%s': %v", outputBasePath, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + overallLogs.WriteString(fmt.Sprintf("Using custom local output path: %s\n", outputBasePath)) + } + + finalImageTags := make(map[string][]string) // serviceName -> tags + + // Collect image IDs and desired tags + if spec.BuildConfig.ComposeFile != "" { + // Get tags from the built compose services + for serviceName, serviceOutput := range result.ServiceOutputs { + // Generate default tag if none specific + defaultTag := fmt.Sprintf("%s_%s:latest", spec.Name, serviceName) + finalImageTags[serviceName] = []string{defaultTag} // Simple tagging for now + // We could potentially read custom tags from the compose file's build section + // Apply tags to the image + for _, tag := range finalImageTags[serviceName] { + if err := s.dockerClient.ImageTag(ctx, serviceOutput.ImageID, tag); err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to tag image %s for service %s with tag %s: %v\n", serviceOutput.ImageID, serviceName, tag, err)) + } else { + overallLogs.WriteString(fmt.Sprintf("Tagged image %s for service %s with %s\n", serviceOutput.ImageID, serviceName, tag)) + } + } + } + } else if result.ImageID != "" { + // Get tags from the main build config for the single image + mainServiceName := spec.Name + if len(spec.BuildConfig.Tags) > 0 { + finalImageTags[mainServiceName] = spec.BuildConfig.Tags + } else { + // Generate default tag + finalImageTags[mainServiceName] = []string{fmt.Sprintf("%s:%s", spec.Name, spec.Version)} + } + // Apply tags + for _, tag := range finalImageTags[mainServiceName] { + if err := s.dockerClient.ImageTag(ctx, result.ImageID, tag); err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to tag image %s with tag %s: %v\n", result.ImageID, tag, err)) + } else { + overallLogs.WriteString(fmt.Sprintf("Tagged image %s with %s\n", result.ImageID, tag)) + } + } } - // Collecter les artefacts - for _, artifactPath := range spec.Artifacts { - fullPath := filepath.Join(buildDir, artifactPath) - data, err := os.ReadFile(fullPath) - if err != nil { - result.Logs += fmt.Sprintf("\nAvertissement: impossible de lire l'artefact %s: %s", artifactPath, err) - continue + // Save or upload based on OutputTarget + overallLogs.WriteString(fmt.Sprintf("Handling build output target: %s\n", spec.BuildConfig.OutputTarget)) + switch spec.BuildConfig.OutputTarget { + case "b2": + if s.b2Config == nil { + errMsg := "OutputTarget is 'b2' but no config is defined" + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + for serviceName, serviceOutput := range result.ServiceOutputs { + tags := finalImageTags[serviceName] // Get the tags we just applied + overallLogs.WriteString(fmt.Sprintf("Exporting and uploading image for service '%s' (ID: %s) to B2...\n", serviceName, serviceOutput.ImageID)) + // Adapt exportAndUploadImage to handle multiple tags per image + objectNames, err := s.exportAndUploadImage(ctx, serviceOutput.ImageID, serviceName, spec.Version, tags) + if err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to export/upload image for service '%s' to B2: %v\n", serviceName, err)) + // Continue with other images? Or fail? Let's continue but log. + } else { + result.B2ObjectNames = append(result.B2ObjectNames, objectNames...) + overallLogs.WriteString(fmt.Sprintf("Service '%s' image uploaded to B2: %v\n", serviceName, objectNames)) + } } - result.Artifacts[artifactPath] = data + + case "local": + for serviceName, serviceOutput := range result.ServiceOutputs { + imageFileName := fmt.Sprintf("%s_%s.tar", spec.Name, serviceName) // Consistent naming + localImagePath := filepath.Join(outputBasePath, imageFileName) + overallLogs.WriteString(fmt.Sprintf("Saving image for service '%s' (ID: %s) locally to %s...\n", serviceName, serviceOutput.ImageID, localImagePath)) + + err := s.saveImageLocally(ctx, serviceOutput.ImageID, localImagePath) + if err != nil { + errMsg := fmt.Sprintf("error during the service image saving locally '%s': %v", serviceName, err) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + result.LocalImagePaths[serviceName] = localImagePath + overallLogs.WriteString(fmt.Sprintf("Service '%s' image saved successfully.\n", serviceName)) + } + case "docker": + // Images are already in the local Docker daemon, tagged. Nothing more to do here. + overallLogs.WriteString("Output target is 'docker', images are available in local daemon.\n") + default: + errMsg := fmt.Sprintf("OutputTarget not supported: %s", spec.BuildConfig.OutputTarget) + result.Success = false + result.ErrorMessage = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) } - // Exporter l'image et l'envoyer au bucket B2 si configuré - if s.b2Config != nil { - objectNames, err := s.exportAndUploadImage(ctx, imageID, spec) + // --- 9. Generate *.run.yml --- + if spec.RunConfigDef.Generate { + overallLogs.WriteString("Generating *.run.yml file...\n") + runConfigPath := filepath.Join(outputBasePath, fmt.Sprintf("%s-%s.run.yml", spec.Name, spec.Version)) + + // Loading the project if it's compose + var parsedComposeProject *ComposeProject // Using a simplified type + if spec.BuildConfig.ComposeFile != "" { + composeFilePath := filepath.Join(buildDir, spec.BuildConfig.ComposeFile) // Chemin dans le contexte de build temporaire + composeData, err := os.ReadFile(composeFilePath) + if err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to read compose file '%s' for run.yml generation: %v\n", composeFilePath, err)) + } else { + parsedComposeProject, err = LoadComposeFile(composeData) + if err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to parse compose file for run.yml generation: %v\n", err)) + parsedComposeProject = nil + } + } + } + + runYAML, err := s.generateRunYAML(ctx, spec, result, finalRuntimeEnv, finalImageTags, parsedComposeProject) if err != nil { - result.Logs += fmt.Sprintf("\nAvertissement lors de l'export vers B2: %s", err) + errMsg := fmt.Sprintf("error during the run.yml generating: %v", err) + overallLogs.WriteString(fmt.Sprintf("Warning: %s\n", errMsg)) + } else if runYAML != nil && len(runYAML.Services) > 0 { + yamlData, err := yaml.Marshal(runYAML) + if err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to parse run file for run.yml generation: %v\n", err)) + } + os.WriteFile(runConfigPath, yamlData, 0755) } else { - result.B2ObjectNames = objectNames + overallLogs.WriteString("Skipping writing run.yml as no services were generated.\n") } } + // --- 10. Finalize --- result.Success = true result.BuildTime = time.Since(startTime).Seconds() + result.Logs = overallLogs.String() // Assign collected logs + + // Clean up temporary build step images (optional) + // Could add logic here to remove images tagged like *-step-* + + overallLogs.WriteString(fmt.Sprintf("Build finished successfully in %.2f seconds.\n", result.BuildTime)) + return result, nil } -// fetchCodebase récupère une codebase selon sa configuration +// --- Helper Functions --- + +// fetching codebase from the provided source type and config func (s *BuildService) fetchCodebase(ctx context.Context, config CodebaseConfig, destDir string) error { - if err := os.MkdirAll(destDir, 0755); err != nil { - return err + // Ensure the parent directory exists, but destDir itself should not exist for git clone + parentDir := filepath.Dir(destDir) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return fmt.Errorf("cannot create the parent directory '%s': %w", parentDir, err) } switch config.SourceType { case "git": - return s.fetchGitRepo(ctx, config, destDir) + // Remove destination dir if it exists before cloning + if _, err := os.Stat(destDir); err == nil { + if err := os.RemoveAll(destDir); err != nil { + return fmt.Errorf("cannot clean the destination dir for repository fetching '%s': %w", destDir, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("error during the verification of the destination directory '%s': %w", destDir, err) + } + return s.fetchGitRepoWithGoGit(ctx, config, destDir) case "local": + // copyLocalDir expects destDir to exist + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("cannot create the destination dir '%s' for the local copy: %w", destDir, err) + } return s.copyLocalDir(config.Source, destDir) case "archive": + // extractArchive expects destDir to exist + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("cannot create the destination dir '%s' for the archive: %w", destDir, err) + } return s.extractArchive(config.Source, destDir) case "buffer": - // Traitement des données directement en mémoire if len(config.Content) == 0 { - return fmt.Errorf("contenu vide pour la codebase de type buffer") + return fmt.Errorf("empty content for the buffer codebase type '%s'", config.Name) + } + // extractBufferToDir expects destDir to exist + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("cannot create the destination dir '%s' for the buffer: %w", destDir, err) } return s.extractBufferToDir(config.Content, destDir) default: - return fmt.Errorf("type de source non pris en charge: %s", config.SourceType) + return fmt.Errorf("this source type is not implemented yet '%s' for the codebase '%s'", config.SourceType, config.Name) } } -// fetchGitRepo clone un dépôt git -func (s *BuildService) fetchGitRepo(ctx context.Context, config CodebaseConfig, destDir string) error { - // Utiliser gitImplementation directe si disponible, sinon utiliser exec.Command - // Ceci est un exemple simplifié qui utilise exec.Command +// cloning repository using the go-git API +func (s *BuildService) fetchGitRepoWithGoGit(ctx context.Context, config CodebaseConfig, destDir string) error { + options := &git.CloneOptions{ + URL: config.Source, + Progress: os.Stdout, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, + Auth: nil, // TODO: Implement authentication + RemoteName: "origin", + Depth: 0, // Clone full history by default + } - cmd := fmt.Sprintf("git clone") if config.Branch != "" { - cmd += fmt.Sprintf(" -b %s", config.Branch) + options.ReferenceName = plumbing.NewBranchReferenceName(config.Branch) + options.SingleBranch = true + options.Depth = 1 } - if config.Commit != "" { - cmd += " --single-branch" + + parentDir := filepath.Dir(destDir) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return fmt.Errorf("cannot create the parent dir '%s': %w", parentDir, err) + } + if _, err := os.Stat(destDir); err == nil { + fmt.Printf("Removing existing directory before clone: %s\n", destDir) + if err := os.RemoveAll(destDir); err != nil { + return fmt.Errorf("failed to remove the dest dir before cloning the repository '%s': %w", destDir, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("error during the dest repertory verification '%s': %w", destDir, err) + } + + fmt.Printf("Cloning repository %s to %s...\n", config.Source, destDir) + repo, err := git.PlainCloneContext(ctx, destDir, false, options) + if err != nil { + // Handle specific errors + if err == transport.ErrAuthenticationRequired { + return fmt.Errorf("authentication require for the codebase fetching '%s'. configure an auth provider (SSH key, token HTTPS)", config.Source) + } + if strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("the repertory '%s' already existing (post verification error): %w", destDir, err) + } + return fmt.Errorf("error during the repository cloning '%s' (branch: %s): %w", config.Source, config.Branch, err) } - cmd += fmt.Sprintf(" %s %s", config.Source, destDir) + fmt.Printf("Repository cloned successfully.\n") + + // If a specific commit is requested, check it out + if config.Commit != "" { + fmt.Printf("Attempting to checkout commit %s...\n", config.Commit) + w, err := repo.Worktree() + if err != nil { + return fmt.Errorf("cannot get the repository work tree '%s' after cloning: %w", config.Source, err) + } - // Exécuter la commande git - // Note: Dans une implémentation réelle, il serait préférable d'utiliser un client Git en Go - // comme go-git ou d'intégrer libgit2 via cgo pour éviter les dépendances externes + commitHash := plumbing.NewHash(config.Commit) + checkoutOptions := &git.CheckoutOptions{ + Hash: commitHash, + Force: true, // Force checkout + } + + err = w.Checkout(checkoutOptions) + if err != nil { + fmt.Printf("Initial checkout failed for commit %s (%v), attempting fetch...\n", config.Commit, err) + // Try fetching explicitly, making sure to fetch all heads and tags + // which should bring in the necessary commit object if it exists remotely. + fetchOpts := &git.FetchOptions{ + RefSpecs: []gitconfig.RefSpec{ + // Fetch all branches from the remote into remote-tracking branches + gitconfig.RefSpec("+refs/heads/*:refs/remotes/origin/*"), + // Fetch all tags + gitconfig.RefSpec("+refs/tags/*:refs/tags/*"), + }, + Auth: options.Auth, // Reuse auth method from clone options + Progress: os.Stdout, // Show progress + // Depth: 0, // Ensure full fetch if depth was used in clone? Or rely on default fetch behavior. + } + + errFetch := repo.FetchContext(ctx, fetchOpts) + // git.NoErrAlreadyUpToDate is expected if the commit was already there but checkout failed for other reasons + if errFetch != nil && errFetch != git.NoErrAlreadyUpToDate { + // Log the fetch error, but the primary error is still the checkout failure if retry also fails. + fmt.Printf("Fetch failed: %v\n", errFetch) + // Return combined error information + return fmt.Errorf("error during the checkout of the commit '%s' (%w) and fetch also failed (%v)", config.Commit, err, errFetch) + } else if errFetch == git.NoErrAlreadyUpToDate { + fmt.Println("Fetch reported remote is already up-to-date.") + } else { + fmt.Println("Fetch completed successfully.") + } + + // Retry checkout after fetch + fmt.Printf("Retrying checkout of commit %s after fetch...\n", config.Commit) + err = w.Checkout(checkoutOptions) + if err != nil { + // If it still fails after fetch, the commit might be invalid or unreachable + return fmt.Errorf("error during the checkout of the commit '%s' (after fetch): %w", config.Commit, err) + } + } + fmt.Printf("Successfully checked out commit %s\n", config.Commit) + } - return execCmd(ctx, cmd) + return nil } -// copyLocalDir copie un répertoire local +// Used to copy a local dir/files with appropriate permissions func (s *BuildService) copyLocalDir(source, dest string) error { - entries, err := ioutil.ReadDir(source) + sourceInfo, err := os.Stat(source) + if err != nil { + return err + } + if !sourceInfo.IsDir() { + return fmt.Errorf("the source '%s' doesn't exist", source) + } + + // Ensure dest directory exists with source permissions + if err := os.MkdirAll(dest, sourceInfo.Mode()); err != nil { + return err + } + + entries, err := os.ReadDir(source) if err != nil { return err } @@ -315,19 +1117,32 @@ func (s *BuildService) copyLocalDir(source, dest string) error { sourcePath := filepath.Join(source, entry.Name()) destPath := filepath.Join(dest, entry.Name()) + fileInfo, err := entry.Info() + if err != nil { + return err + } + if entry.IsDir() { - if err := os.MkdirAll(destPath, entry.Mode()); err != nil { + // Recursively copy subdirectory + if err := s.copyLocalDir(sourcePath, destPath); err != nil { return err } - if err := s.copyLocalDir(sourcePath, destPath); err != nil { + } else if fileInfo.Mode()&os.ModeSymlink != 0 { + // Handle symlinks (read link and recreate) - Optional, can be complex + link, err := os.Readlink(sourcePath) + if err != nil { + return err + } + if err := os.Symlink(link, destPath); err != nil { return err } } else { - data, err := ioutil.ReadFile(sourcePath) + // Copy regular file content and permissions + data, err := os.ReadFile(sourcePath) if err != nil { return err } - if err := ioutil.WriteFile(destPath, data, entry.Mode()); err != nil { + if err := os.WriteFile(destPath, data, fileInfo.Mode()); err != nil { return err } } @@ -335,1000 +1150,856 @@ func (s *BuildService) copyLocalDir(source, dest string) error { return nil } -// extractArchive extrait une archive vers un répertoire +// Extract an archive (tar, tar.gz, zip) to a repertory func (s *BuildService) extractArchive(sourcePath string, destDir string) error { - data, err := ioutil.ReadFile(sourcePath) + file, err := os.Open(sourcePath) if err != nil { - return err + return fmt.Errorf("cannot open the archive '%s': %w", sourcePath, err) + } + defer file.Close() + + // Peek at the first few bytes to guess the format + header := make([]byte, 4) + _, err = file.ReadAt(header, 0) + if err != nil && err != io.EOF { + return fmt.Errorf("cannot read the archive header '%s': %w", sourcePath, err) + } + // Reset reader position + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("cannot reset the reading position in the archive '%s': %w", sourcePath, err) + } + + if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { + // Gzip compressed (likely tar.gz) + gzr, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("error during the gzip reader creation for the archive '%s': %w", sourcePath, err) + } + defer gzr.Close() + return extractTar(tar.NewReader(gzr), destDir) + } else if bytes.HasPrefix(header, []byte{0x50, 0x4B, 0x03, 0x04}) { + // ZIP archive + // Need file size for zip reader + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("cannot get the zip file size '%s': %w", sourcePath, err) + } + return extractZip(file, fileInfo.Size(), destDir) // Implement extractZip + } else { + // Assume plain tar + return extractTar(tar.NewReader(file), destDir) } - return s.extractBufferToDir(data, destDir) } -// extractBufferToDir extrait des données d'archive en mémoire vers un répertoire +// Extract a buffer slice to a dir func (s *BuildService) extractBufferToDir(data []byte, destDir string) error { - // Détecter le type d'archive et extraire + dataReader := bytes.NewReader(data) + if bytes.HasPrefix(data, []byte{0x1F, 0x8B}) { // Archive gzip (tar.gz) - gzr, err := gzip.NewReader(bytes.NewReader(data)) + gzr, err := gzip.NewReader(dataReader) if err != nil { - return err + return fmt.Errorf("error during the archive reading from the buffer: %w", err) } defer gzr.Close() - return extractTar(tar.NewReader(gzr), destDir) } else if bytes.HasPrefix(data, []byte{0x50, 0x4B, 0x03, 0x04}) { // Archive ZIP - // Note: Pour une implémentation complète, utilisez archive/zip - return fmt.Errorf("extraction ZIP non implémentée, utilisez tar.gz") + return extractZip(dataReader, int64(len(data)), destDir) // Implement extractZip for ReaderAt } else { // Supposer tar simple - return extractTar(tar.NewReader(bytes.NewReader(data)), destDir) + return extractTar(tar.NewReader(dataReader), destDir) } } -// extractTar extrait une archive tar +// Extract a tar archive func extractTar(tr *tar.Reader, destDir string) error { for { header, err := tr.Next() if err == io.EOF { - break + break // End of archive } if err != nil { - return err + return fmt.Errorf("error during the tar entry reading: %w", err) } + // Sanitize the target path to prevent path traversal vulnerabilities target := filepath.Join(destDir, header.Name) + if !strings.HasPrefix(target, filepath.Clean(destDir)+string(os.PathSeparator)) { + return fmt.Errorf("invalid tar content: '%s' trying to get out from the source repertory", header.Name) + } + + // Get file info from header + info := header.FileInfo() switch header.Typeflag { case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { - return err + if err := os.MkdirAll(target, info.Mode()); err != nil { + return fmt.Errorf("cannot create the repertory for the tar '%s': %w", target, err) } case tar.TypeReg: - dir := filepath.Dir(target) - if err := os.MkdirAll(dir, 0755); err != nil { - return err + // Ensure parent directory exists + parentDir := filepath.Dir(target) + if err := os.MkdirAll(parentDir, 0755); err != nil { // Use default mode for parent dirs + return fmt.Errorf("cannot the parent directory '%s' for the tar file: %w", parentDir, err) } - f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + // Create the file + file, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) if err != nil { - return err + return fmt.Errorf("cannot create the tar file '%s': %w", target, err) } - - if _, err := io.Copy(f, tr); err != nil { - f.Close() - return err + // Copy contents + _, err = io.Copy(file, tr) + file.Close() // Close immediately after copy + if err != nil { + return fmt.Errorf("error during the tar content copying '%s': %w", target, err) } - f.Close() - } + case tar.TypeSymlink: + // Recreate symlink + if err := os.Symlink(header.Linkname, target); err != nil { + return fmt.Errorf("cannot create the symblink for the tar '%s' -> '%s': %w", target, header.Linkname, err) + } + case tar.TypeLink: + // Handle hard links (less common, might require mapping) - Skip for now + fmt.Printf("Warning: Hard link extraction not fully supported (from %s to %s)\n", header.Name, header.Linkname) + default: + // Skip other types (char device, block device, fifo) + fmt.Printf("Warning: Skipping unsupported tar entry type %c for %s\n", header.Typeflag, header.Name) + } } return nil } -// execCmd exécute une commande shell -func execCmd(ctx context.Context, cmdStr string) error { - // Dans une implémentation réelle, cette fonction utiliserait os/exec - // Pour l'exemple, on simule un retour réussi +// Extract a zip archive +func extractZip(r io.ReaderAt, size int64, destDir string) error { + zr, err := zip.NewReader(r, size) + if err != nil { + return fmt.Errorf("error during the zip opening: %w", err) + } + + for _, f := range zr.File { + // Sanitize the target path + targetPath := filepath.Join(destDir, f.Name) + if !strings.HasPrefix(targetPath, filepath.Clean(destDir)+string(os.PathSeparator)) { + return fmt.Errorf("invalid content: '%s' trying to get out from the target repertory", f.Name) + } + + if f.FileInfo().IsDir() { + if err := os.MkdirAll(targetPath, f.Mode()); err != nil { + return fmt.Errorf("cannot create the zip repertory '%s': %w", targetPath, err) + } + continue + } + + // Ensure parent directory exists + parentDir := filepath.Dir(targetPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return fmt.Errorf("cannot create the parent repertory '%s' for the zip file: %w", parentDir, err) + } + + // Open the file inside the zip archive + rc, err := f.Open() + if err != nil { + return fmt.Errorf("cannot open the file '%s' in the zip: %w", f.Name, err) + } + + // Create the destination file + outFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + rc.Close() + return fmt.Errorf("cannot create the targeting zip file '%s': %w", targetPath, err) + } + + // Copy the content + _, err = io.Copy(outFile, rc) + + // Close files + outFile.Close() + rc.Close() + + if err != nil { + return fmt.Errorf("error during the zip content copying '%s': %w", f.Name, err) + } + } + return nil +} + +// Resource downloader +func (s *BuildService) downloadFile(ctx context.Context, url, targetPath string) error { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return fmt.Errorf("error during the request creation %s: %w", url, err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("error during the GET request for the resource URL %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed downloading of %s: status %s", url, resp.Status) + } + + file, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("cannot create the target file %s: %w", targetPath, err) + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return fmt.Errorf("error during the target path writing %s: %w", targetPath, err) + } + return nil } -// buildWithDocker construit une image avec l'API Docker -func (s *BuildService) buildWithDocker(ctx context.Context, buildDir, dockerfilePath string, spec *BuildSpec) (string, string, error) { +// Build a single image from a context and a specific Config +func (s *BuildService) buildSingleImage(ctx context.Context, buildContextDir string, dockerfilePath string, spec *BuildSpec) (string, string, error) { var logBuffer bytes.Buffer // Créer le contexte de build en mémoire (tar) - buildContextTar, err := archive.TarWithOptions(buildDir, &archive.TarOptions{}) + // Exclude .git by default? Or rely on .dockerignore? Let's rely on .dockerignore for now. + buildContextTar, err := archive.TarWithOptions(buildContextDir, &archive.TarOptions{}) if err != nil { - return "", "", fmt.Errorf("erreur lors de la création du contexte: %w", err) + return "", logBuffer.String(), fmt.Errorf("erreur lors de la création du contexte tar pour '%s': %w", buildContextDir, err) } defer buildContextTar.Close() // Préparer les options de build buildOptions := types.ImageBuildOptions{ - Dockerfile: filepath.Base(dockerfilePath), - Tags: spec.BuildConfig.Tags, - Remove: true, - NoCache: spec.BuildConfig.NoCache, - BuildArgs: make(map[string]*string), + Dockerfile: filepath.Base(dockerfilePath), // Dockerfile name relative to context root + Tags: spec.BuildConfig.Tags, // Tags defined in the main spec or step spec + Remove: true, // Remove intermediate containers + ForceRemove: true, + NoCache: spec.BuildConfig.NoCache, + BuildArgs: make(map[string]*string), + PullParent: spec.BuildConfig.Pull, // Tenter de pull l'image de base + Version: types.BuilderBuildKit, // Préférer BuildKit si disponible + // TODO: Add Platform handling spec.BuildConfig.Platforms + } + if !spec.BuildConfig.BuildKit { + buildOptions.Version = types.BuilderV1 // Force legacy builder if requested } - // Ajouter les arguments de build + // Ajouter les arguments de build (variables d'env du spec peuvent être utilisées ici si préfixées ou explicitement mappées) for k, v := range spec.BuildConfig.Args { - value := v // Copie locale pour éviter les problèmes de pointeur + value := v // Copie locale buildOptions.BuildArgs[k] = &value } + // Injecter les variables d'env comme build args (optionnel, dépend de l'usage) + // for k, v := range mergedEnv { // Use the merged env from Build() scope if needed + // if _, exists := buildOptions.BuildArgs[k]; !exists { + // value := v + // buildOptions.BuildArgs[k] = &value + // } + // } - // Ajouter le target si spécifié if spec.BuildConfig.Target != "" { buildOptions.Target = spec.BuildConfig.Target } // Exécuter le build + fmt.Fprintf(&logBuffer, "Starting Docker build with context: %s, Dockerfile: %s\n", buildContextDir, dockerfilePath) buildResponse, err := s.dockerClient.ImageBuild(ctx, buildContextTar, buildOptions) if err != nil { - return "", "", fmt.Errorf("erreur lors du build Docker: %w", err) + // Try falling back to legacy builder if BuildKit failed? + if spec.BuildConfig.BuildKit && strings.Contains(err.Error(), "BuildKit") { + fmt.Fprintf(&logBuffer, "BuildKit build failed, trying legacy builder...\n") + buildOptions.Version = types.BuilderV1 + buildResponse, err = s.dockerClient.ImageBuild(ctx, buildContextTar, buildOptions) + } + if err != nil { + logBuffer.WriteString(fmt.Sprintf("\nDocker build command failed: %v\n", err)) + return "", logBuffer.String(), fmt.Errorf("erreur lors du lancement du build Docker: %w", err) + } } defer buildResponse.Body.Close() - // Lire et traiter la sortie + // Lire et traiter la sortie JSON var imageID string decoder := json.NewDecoder(buildResponse.Body) for { var msg jsonmessage.JSONMessage if err := decoder.Decode(&msg); err != nil { if err == io.EOF { - break + break // Fin normale du stream } - return "", logBuffer.String(), err - } - - // Traiter les messages - if msg.Error != nil { - return "", logBuffer.String(), fmt.Errorf("%s", msg.Error.Message) + // Log incomplete stream error? + logBuffer.WriteString(fmt.Sprintf("\nError decoding build response stream: %v\n", err)) + // Return success if we already got an image ID? Or fail? Let's fail. + if imageID == "" { + return "", logBuffer.String(), fmt.Errorf("erreur de décodage du flux de build et aucun ID d'image obtenu: %w", err) + } + break // Break but potentially return success if imageID was found } if msg.Stream != "" { fmt.Fprint(&logBuffer, msg.Stream) - // Extraire l'ID de l'image - if strings.Contains(msg.Stream, "Successfully built") { + // Try to parse image ID from common "Successfully built " messages + if strings.Contains(msg.Stream, "Successfully built ") { parts := strings.Fields(msg.Stream) - for i, part := range parts { - if part == "built" && i < len(parts)-1 { - imageID = parts[i+1] - break - } + if len(parts) >= 3 && parts[0] == "Successfully" && parts[1] == "built" { + // Handle potential sha256: prefix + id := strings.TrimPrefix(parts[2], "sha256:") + imageID = id } } + // Docker Engine API v1.31+ may send ID in "Successfully tagged " + if strings.Contains(msg.Stream, "Successfully tagged ") && imageID == "" { + // Less reliable way to get ID if build is tagged, might need inspection later + } + } else if msg.Status != "" { + logLine := msg.Status + if msg.Progress != nil { + logLine += " " + msg.Progress.String() + } + if msg.ID != "" { + logLine = fmt.Sprintf("[%s] %s", msg.ID, logLine) + } + fmt.Fprintln(&logBuffer, logLine) + } + + // Check for build errors reported in the stream + if msg.Error != nil { + logBuffer.WriteString(fmt.Sprintf("\nBuild Error: %s\n", msg.Error.Message)) + return "", logBuffer.String(), fmt.Errorf("erreur dans le flux de build: %s", msg.Error.Message) } - // Traiter les messages auxiliaires + // Extract Image ID from Aux message (often contains the final sha256 ID) if msg.Aux != nil { var auxMsg struct { ID string `json:"ID"` } if err := json.Unmarshal(*msg.Aux, &auxMsg); err == nil && auxMsg.ID != "" { - imageID = auxMsg.ID + // Prefer the ID from Aux if available + id := strings.TrimPrefix(auxMsg.ID, "sha256:") + imageID = id } } - } + } // End stream reading loop - return imageID, logBuffer.String(), nil -} - -// getImageSize récupère la taille d'une image Docker -func (s *BuildService) getImageSize(ctx context.Context, imageID string) (int64, error) { - summary, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) - if err != nil { - return 0, err - } - return summary.Size, nil -} - -// exportAndUploadImage exporte une image Docker et l'upload vers B2 -func (s *BuildService) exportAndUploadImage(ctx context.Context, imageID string, spec *BuildSpec) ([]string, error) { - if s.b2Config == nil { - return nil, fmt.Errorf("configuration B2 non définie") - } - - // Créer un reader pour l'image exportée - reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) - if err != nil { - return nil, fmt.Errorf("erreur lors de l'export de l'image: %w", err) - } - defer reader.Close() - - // Lire l'image en mémoire - imageData, err := io.ReadAll(reader) - if err != nil { - return nil, fmt.Errorf("erreur lors de la lecture de l'image: %w", err) - } - - // Initialiser le client B2 - ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) - defer cancel() - - b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) - if err != nil { - return nil, fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) - } - - // Accéder au bucket - bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) - if err != nil { - panic(err) + if imageID == "" { + // If no ID found after successful stream processing, maybe inspect the tag? + if len(buildOptions.Tags) > 0 { + inspected, err := s.getImageInfoByTag(ctx, buildOptions.Tags[0]) + if err == nil { + imageID = inspected.ID + fmt.Fprintf(&logBuffer, "\nImage ID retrieved via tag inspection: %s\n", imageID) + } else { + logBuffer.WriteString("\nBuild stream finished, but no image ID found and tag inspection failed.\n") + return "", logBuffer.String(), fmt.Errorf("build terminé mais impossible de déterminer l'ID de l'image finale") + } + } else { + logBuffer.WriteString("\nBuild stream finished, but no image ID found (and no tags specified).\n") + return "", logBuffer.String(), fmt.Errorf("build terminé mais impossible de déterminer l'ID de l'image finale (aucun tag)") + } } - // Générer un nom d'objet basé sur l'ID de l'image et les tags - imageName := fmt.Sprintf("%s-%s.tar", spec.Name, spec.Version) - objectPath := filepath.Join(s.b2Config.BasePath, imageName) + // Clean the image ID (remove potential sha256: prefix if still there) + imageID = strings.TrimPrefix(imageID, "sha256:") - // Uploader l'image - obj := bucket.Object(objectPath) - writer := obj.NewWriter(ctx) + fmt.Fprintf(&logBuffer, "\nBuild successful. Final Image ID: %s\n", imageID) + return imageID, logBuffer.String(), nil +} - if _, err := writer.Write(imageData); err != nil { - writer.Close() - return nil, fmt.Errorf("erreur lors de l'écriture vers B2: %w", err) - } +// buildComposeProject itère sur les services d'un projet Compose et les construit +func (s *BuildService) buildComposeProject(ctx context.Context, buildDir string, project *ComposeProject, spec *BuildSpec, result *BuildResult, overallLogs *strings.Builder) []string { + var buildErrors []string + composeFileDir := filepath.Dir(filepath.Join(buildDir, spec.BuildConfig.ComposeFile)) // Directory containing the compose file + + for Name, service := range project.Services { + if service.Build == nil { + // Service uses an existing image, maybe pull it? + if service.Image != "" { + overallLogs.WriteString(fmt.Sprintf("Service '%s' uses image '%s'. Pulling...\n", Name, service.Image)) + if err := s.pullImage(ctx, service.Image, overallLogs); err != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: Failed to pull image '%s' for service '%s': %v\n", service.Image, Name, err)) + // Continue or fail? Let's continue. + } + } else { + overallLogs.WriteString(fmt.Sprintf("Service '%s' has no 'build' section and no 'image' specified. Skipping build.\n", Name)) + } + continue + } - if err := writer.Close(); err != nil { - return nil, fmt.Errorf("erreur lors de la finalisation de l'upload B2: %w", err) - } + overallLogs.WriteString(fmt.Sprintf("--- Building Service: %s ---\n", Name)) - // Pour chaque tag, exporter aussi une référence - objectNames := []string{objectPath} - for _, tag := range spec.BuildConfig.Tags { - // Nettoyer le tag pour en faire un nom de fichier valide - cleanTag := strings.ReplaceAll(tag, ":", "-") - cleanTag = strings.ReplaceAll(cleanTag, "/", "-") + // Determine build context and Dockerfile path relative to the compose file directory + contextPath := service.Build.Context + if contextPath == "" || contextPath == "." { + contextPath = composeFileDir + } else if !filepath.IsAbs(contextPath) { + contextPath = filepath.Join(composeFileDir, contextPath) + } + // Clean the path + contextPath = filepath.Clean(contextPath) - tagPath := filepath.Join(s.b2Config.BasePath, cleanTag+".tar") + dockerfilePath := service.Build.Dockerfile + if dockerfilePath == "" { + dockerfilePath = "Dockerfile" // Default Dockerfile name + } + // Dockerfile path is relative to the context path + fullDockerfilePath := filepath.Join(contextPath, dockerfilePath) + + overallLogs.WriteString(fmt.Sprintf("Service '%s': Context='%s', Dockerfile='%s'\n", Name, contextPath, fullDockerfilePath)) + + // Create a temporary BuildSpec for this service build + serviceSpec := &BuildSpec{ + Name: fmt.Sprintf("%s-%s-service-%s", spec.Name, spec.Version, Name), + Version: "latest", // Or derive from main spec? + BuildConfig: BuildConfig{ + Args: make(map[string]string), // Start with empty args + NoCache: spec.BuildConfig.NoCache, // Inherit NoCache setting + Target: service.Build.Target, // Inherit Target setting + Pull: spec.BuildConfig.Pull, // Inherit Pull setting + Tags: []string{fmt.Sprintf("%s:latest", Name)}, // Default tag for the service image + // Use buildkit setting from main spec? + BuildKit: spec.BuildConfig.BuildKit, + }, + } - // Créer un lien symbolique/référence dans B2 - // Note: B2 ne supporte pas directement les liens symboliques, on pourrait soit: - // 1. Uploader à nouveau l'image (gaspillage d'espace) - // 2. Créer un petit fichier de référence qui pointe vers l'image principale + // Add build args from main spec first + for k, v := range spec.BuildConfig.Args { + serviceSpec.BuildConfig.Args[k] = v + } + // Override/add with build args from compose file service.build.args + if service.Build.Args != nil { + for k, v := range service.Build.Args { + // Compose args can be string pointers, handle nil + if v != nil { + serviceSpec.BuildConfig.Args[k] = *v + } else { + // Handle case where arg is defined but has no value (e.g., ARG name) + // We might need to resolve these from the environment? + // For now, let's just skip them or assign an empty string? + // buildOptions.BuildArgs expects map[string]*string, so nil is possible. + serviceSpec.BuildConfig.Args[k] = "" // Or handle differently? + } + } + } - refObj := bucket.Object(tagPath) - refWriter := refObj.NewWriter(ctx) + // Build the image for the service + imageID, logs, err := s.buildSingleImage(ctx, contextPath, fullDockerfilePath, serviceSpec) + overallLogs.WriteString(fmt.Sprintf("Logs for service %s:\n%s\n", Name, logs)) - refContent := fmt.Sprintf("Reference to: %s\nImage ID: %s\n", objectPath, imageID) - if _, err := refWriter.Write([]byte(refContent)); err != nil { - refWriter.Close() - return objectNames, err // Retourner les objets déjà uploadés + if err != nil { + errMsg := fmt.Sprintf("erreur lors du build du service '%s': %v", Name, err) + buildErrors = append(buildErrors, errMsg) + overallLogs.WriteString(errMsg + "\n") + // Store partial results? + result.ServiceOutputs[Name] = ServiceOutput{Logs: logs} + continue // Continue to build other services even if one fails } - if err := refWriter.Close(); err != nil { - return objectNames, err + imageSize, sizeErr := s.getImageSize(ctx, imageID) + if sizeErr != nil { + overallLogs.WriteString(fmt.Sprintf("Warning: could not get size for image %s (service %s): %v\n", imageID, Name, sizeErr)) } - objectNames = append(objectNames, tagPath) - } - - return objectNames, nil -} - -// CreateContainer crée un conteneur à partir d'une image -func (s *BuildService) CreateContainer(ctx context.Context, imageID string, config *container.Config, hostConfig *container.HostConfig) (string, error) { - if config == nil { - config = &container.Config{ - Image: imageID, + // Store results for this service + result.ImageIDs[Name] = imageID + result.ImageSizes[Name] = imageSize + result.ServiceOutputs[Name] = ServiceOutput{ + ImageID: imageID, + ImageSize: imageSize, + Logs: logs, } - } else if config.Image == "" { - config.Image = imageID - } + overallLogs.WriteString(fmt.Sprintf("Service '%s' built successfully. ImageID: %s, Size: %d\n", Name, imageID, imageSize)) + overallLogs.WriteString(fmt.Sprintf("--- Finished Service: %s ---\n", Name)) - resp, err := s.dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, "") - if err != nil { - return "", err - } + } // End loop over services - return resp.ID, nil + return buildErrors } -// RunContainer démarre un conteneur et attend sa terminaison -func (s *BuildService) RunContainer(ctx context.Context, containerID string) (string, error) { - if err := s.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { - return "", err +// pullImage pulls a Docker image if it doesn't exist locally +func (s *BuildService) pullImage(ctx context.Context, imageName string, logs io.Writer) error { + // Check if image exists locally first to avoid unnecessary pulls + _, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageName) + if err == nil { + fmt.Fprintf(logs, "Image '%s' already exists locally.\n", imageName) + return nil // Image found } - - // Attendre que le conteneur se termine - statusCh, errCh := s.dockerClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) - - var logs bytes.Buffer - - // Récupérer les logs du conteneur - logsOptions := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, + if !client.IsErrNotFound(err) { + // Different error during inspection + return fmt.Errorf("erreur lors de l'inspection de l'image '%s' avant pull: %w", imageName, err) } - logsReader, err := s.dockerClient.ContainerLogs(ctx, containerID, logsOptions) + // Image not found, proceed to pull + fmt.Fprintf(logs, "Pulling image '%s'...\n", imageName) + reader, err := s.dockerClient.ImagePull(ctx, imageName, image.PullOptions{}) if err != nil { - return "", err + return fmt.Errorf("erreur lors du lancement du pull de l'image '%s': %w", imageName, err) } - defer logsReader.Close() + defer reader.Close() - // Copier les logs dans notre buffer - _, err = stdcopy.StdCopy(&logs, &logs, logsReader) + // Write pull progress to logs + termFd, isTerm := term.GetFdInfo(logs) // Check if logs is a terminal for progress bars + err = jsonmessage.DisplayJSONMessagesStream(reader, logs, termFd, isTerm, nil) if err != nil { - return logs.String(), err + return fmt.Errorf("erreur lors de la lecture du flux de pull pour l'image '%s': %w", imageName, err) } - // Attendre la fin du conteneur - select { - case err := <-errCh: - return logs.String(), err - case <-statusCh: - return logs.String(), nil - } + fmt.Fprintf(logs, "Image '%s' pulled successfully.\n", imageName) + return nil } -// ExportContainer exporte un conteneur vers un fichier tar -func (s *BuildService) ExportContainer(ctx context.Context, containerID string) ([]byte, error) { - // Exporter le conteneur - readCloser, err := s.dockerClient.ContainerExport(ctx, containerID) +// getImageSize récupère la taille d'une image Docker +func (s *BuildService) getImageSize(ctx context.Context, imageID string) (int64, error) { + // Use the image ID (which should be sha256 or short ID) for inspection + summary, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) if err != nil { - return nil, err + return 0, fmt.Errorf("erreur d'inspection de l'image '%s': %w", imageID, err) } - defer readCloser.Close() - - // Lire tous les données du conteneur - return io.ReadAll(readCloser) + return summary.Size, nil } -// Cleanup nettoie les ressources Docker (images, conteneurs) -func (s *BuildService) Cleanup(ctx context.Context, olderThan time.Duration) error { - // Supprimer les conteneurs arrêtés - containers, err := s.dockerClient.ContainerList(ctx, container.ListOptions{All: true}) - if err != nil { - return err - } - - now := time.Now() - for _, c := range containers { - createdTime := time.Unix(c.Created, 0) - if now.Sub(createdTime) > olderThan { - // Supprimer le conteneur - err := s.dockerClient.ContainerRemove(ctx, c.ID, container.RemoveOptions{ - Force: true, - }) - if err != nil { - // Log l'erreur mais continuer - fmt.Printf("Erreur lors de la suppression du conteneur %s: %v\n", c.ID, err) - } - } - } - - // Supprimer les images non utilisées - pruneReport, err := s.dockerClient.ImagesPrune(ctx, filters.NewArgs( - filters.Arg("until", fmt.Sprintf("%dm", int(olderThan.Minutes()))), - )) +// getImageInfoByTag récupère les infos d'une image par son tag +func (s *BuildService) getImageInfoByTag(ctx context.Context, imageTag string) (*types.ImageInspect, error) { + summary, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageTag) if err != nil { - return err + return nil, fmt.Errorf("erreur d'inspection de l'image taggée '%s': %w", imageTag, err) } - - fmt.Printf("Images nettoyées: %d, espace libéré: %d bytes\n", - len(pruneReport.ImagesDeleted), pruneReport.SpaceReclaimed) - - return nil + return &summary, nil } -// UploadArtifactToB2 upload un artefact vers B2 -func (s *BuildService) UploadArtifactToB2(ctx context.Context, data []byte, objectPath string) (string, error) { - if s.b2Config == nil { - return "", fmt.Errorf("configuration B2 non définie") - } - - // Initialiser le client B2 - b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) +// saveImageLocally sauvegarde une image Docker dans un fichier .tar local +func (s *BuildService) saveImageLocally(ctx context.Context, imageID string, targetPath string) error { + reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) if err != nil { - return "", fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + return fmt.Errorf("erreur lors de l'export de l'image '%s': %w", imageID, err) } + defer reader.Close() - // Accéder au bucket - bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + file, err := os.Create(targetPath) if err != nil { - panic(err) - } - - // Préparer le chemin complet - fullPath := filepath.Join(s.b2Config.BasePath, objectPath) - - // Uploader l'artefact - obj := bucket.Object(fullPath) - writer := obj.NewWriter(ctx) - - // Définir le type de contenu si possible - // if ext := filepath.Ext(objectPath); ext != "" { - // switch strings.ToLower(ext) { - // case ".tar": - // writer.SetContentType("application/x-tar") - // case ".gz", ".tgz": - // writer.SetContentType("application/gzip") - // case ".zip": - // writer.SetContentType("application/zip") - // case ".json": - // writer.SetContentType("application/json") - // case ".txt": - // writer.SetContentType("text/plain") - // } - // } - - if _, err := writer.Write(data); err != nil { - writer.Close() - return "", fmt.Errorf("erreur lors de l'écriture vers B2: %w", err) + return fmt.Errorf("impossible de créer le fichier image local '%s': %w", targetPath, err) } + defer file.Close() - if err := writer.Close(); err != nil { - return "", fmt.Errorf("erreur lors de la finalisation de l'upload B2: %w", err) + _, err = io.Copy(file, reader) + if err != nil { + return fmt.Errorf("erreur lors de l'écriture dans le fichier image local '%s': %w", targetPath, err) } - return fullPath, nil + return nil } -// DownloadImageFromB2 télécharge une image Docker depuis B2 -func (s *BuildService) DownloadImageFromB2(ctx context.Context, objectPath string) (string, error) { +// exportAndUploadImage exporte une image Docker et l'upload vers B2 (modifié pour nom/version/tags) +func (s *BuildService) exportAndUploadImage(ctx context.Context, imageID, serviceName, version string, tags []string) ([]string, error) { if s.b2Config == nil { - return "", fmt.Errorf("configuration B2 non définie") - } - - // Initialiser le client B2 - b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) - if err != nil { - return "", fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + return nil, fmt.Errorf("configuration B2 non définie pour upload") } - // Accéder au bucket - bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + // Créer un reader pour l'image exportée + reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) // Use the actual image ID if err != nil { - panic(err) + return nil, fmt.Errorf("erreur lors de l'export de l'image ID '%s': %w", imageID, err) } - - // Préparer le chemin complet - fullPath := objectPath - if !strings.HasPrefix(objectPath, s.b2Config.BasePath) { - fullPath = filepath.Join(s.b2Config.BasePath, objectPath) - } - - // Télécharger l'objet - obj := bucket.Object(fullPath) - reader := obj.NewReader(ctx) defer reader.Close() - // Lire l'objet en mémoire - imageData, err := io.ReadAll(reader) - if err != nil { - return "", fmt.Errorf("erreur lors de la lecture depuis B2: %w", err) - } + // Utiliser io.Pipe pour streamer directement vers B2 sans charger en mémoire (plus efficace pour grosses images) + pr, pw := io.Pipe() - // Charger l'image dans Docker - loadResp, err := s.dockerClient.ImageLoad(ctx, bytes.NewReader(imageData)) - if err != nil { - return "", fmt.Errorf("erreur lors du chargement de l'image: %w", err) - } - defer loadResp.Body.Close() - - // Lire la réponse pour obtenir l'ID de l'image - var loadOutput struct { - Stream string `json:"stream"` - } + var uploadErr error + var wg sync.WaitGroup + wg.Add(1) - decoder := json.NewDecoder(loadResp.Body) - imageID := "" - - for { - if err := decoder.Decode(&loadOutput); err != nil { - if err == io.EOF { - break - } - return "", fmt.Errorf("erreur lors de la lecture de la réponse: %w", err) - } + // Goroutine pour uploader depuis le pipe reader + go func() { + defer wg.Done() + defer pr.Close() // Fermer le reader quand l'upload est fini ou échoue - // Extraire l'ID de l'image si présent - if strings.Contains(loadOutput.Stream, "Loaded image") { - parts := strings.Fields(loadOutput.Stream) - if len(parts) > 2 { - imageID = parts[len(parts)-1] - imageID = strings.TrimSpace(imageID) - break - } - } - } - - // Si on n'a pas trouvé d'ID explicite, lister les images pour trouver la plus récente - if imageID == "" { - images, err := s.dockerClient.ImageList(ctx, image.ListOptions{}) + b2Client, err := b2.NewClient(context.WithoutCancel(ctx), s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) // Use context without timeout for upload potentially if err != nil { - return "", fmt.Errorf("erreur lors de la liste des images: %w", err) + uploadErr = fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + return } - if len(images) > 0 { - // Prendre la plus récente (la première de la liste) - imageID = images[0].ID + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + if err != nil { + uploadErr = fmt.Errorf("erreur d'accès au bucket B2 '%s': %w", s.b2Config.BucketName, err) + return } - } - return imageID, nil -} - -// BuildWithMultipleCodebases construit une image Docker à partir de plusieurs codebases -func (s *BuildService) BuildWithMultipleCodebases(ctx context.Context, spec *BuildSpec) (*BuildResult, error) { - // Validation des paramètres - if len(spec.Codebases) < 2 { - return nil, fmt.Errorf("au moins deux codebases sont nécessaires pour cette opération") - } - - return s.Build(ctx, spec) -} + // Nom d'objet principal basé sur service et version + imageName := fmt.Sprintf("%s-%s.tar", serviceName, version) + objectPath := filepath.Join(s.b2Config.BasePath, imageName) -// SaveDockerImageToBuffer exporte une image Docker vers un buffer en mémoire -func (s *BuildService) SaveDockerImageToBuffer(ctx context.Context, imageID string) ([]byte, error) { - // Exporter l'image - reader, err := s.dockerClient.ImageSave(ctx, []string{imageID}) - if err != nil { - return nil, fmt.Errorf("erreur lors de l'export de l'image: %w", err) - } - defer reader.Close() + obj := bucket.Object(objectPath) + writer := obj.NewWriter(ctx) - // Lire les données en mémoire - var buf bytes.Buffer - _, err = io.Copy(&buf, reader) - if err != nil { - return nil, fmt.Errorf("erreur lors de la lecture des données d'image: %w", err) - } + fmt.Printf("Starting B2 upload to %s...\n", objectPath) // Log start + _, err = io.Copy(writer, pr) // Lire depuis le pipe et écrire vers B2 + if err != nil { + writer.Close() // Important to close writer even on error + uploadErr = fmt.Errorf("erreur lors de l'écriture stream vers B2 (%s): %w", objectPath, err) + return + } - return buf.Bytes(), nil -} + err = writer.Close() // Finaliser l'upload + if err != nil { + uploadErr = fmt.Errorf("erreur lors de la finalisation de l'upload B2 (%s): %w", objectPath, err) + return + } + fmt.Printf("Finished B2 upload to %s.\n", objectPath) // Log success + // Upload successful for the main object path + }() -// CreateDockerfileFromMultipleCodebases crée un Dockerfile optimisé pour combiner plusieurs bases de code -func (s *BuildService) CreateDockerfileFromMultipleCodebases(spec *BuildSpec) (string, error) { - if len(spec.Codebases) < 1 { - return "", fmt.Errorf("au moins une codebase est nécessaire") - } + // Goroutine pour copier depuis Docker save vers le pipe writer + var copyErr error + go func() { + defer pw.Close() // Fermer le writer quand la copie est finie ou échoue + _, copyErr = io.Copy(pw, reader) + }() - var dockerfile strings.Builder + // Attendre la fin de l'upload + wg.Wait() - // Si un Dockerfile personnalisé est fourni, l'utiliser tel quel - if spec.BuildConfig.Dockerfile != "" { - return spec.BuildConfig.Dockerfile, nil + // Vérifier les erreurs + if copyErr != nil { + return nil, fmt.Errorf("erreur lors de la lecture des données de l'image Docker: %w", copyErr) } - - // Si aucun Dockerfile personnalisé n'est fourni, en créer un qui combine les codebases - // Utiliser l'image de base spécifiée ou une image par défaut - baseImage := spec.BuildConfig.BaseImage - if baseImage == "" { - baseImage = "alpine:latest" // Image par défaut + if uploadErr != nil { + return nil, fmt.Errorf("erreur lors de l'upload vers B2: %w", uploadErr) } - dockerfile.WriteString(fmt.Sprintf("FROM %s\n\n", baseImage)) - dockerfile.WriteString("WORKDIR /app\n\n") + // L'upload principal a réussi. Maintenant, gérer les tags comme des références (petits fichiers texte). + // Note: B2 ne supporte pas les liens symboliques directs. On crée des fichiers de ref. + objectNames := []string{filepath.Join(s.b2Config.BasePath, fmt.Sprintf("%s-%s.tar", serviceName, version))} // Start with the main path - // Ajouter chaque codebase - for _, codebase := range spec.Codebases { - dockerfile.WriteString(fmt.Sprintf("# Ajout de la codebase %s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ /app/%s/\n\n", codebase.Name, codebase.Name)) + // Re-init client/bucket for tag uploads (ou réutiliser si possible) + b2Client, err := b2.NewClient(ctx, s.b2Config.AccountID, s.b2Config.ApplicationKey, b2.UserAgent("build-service")) + if err != nil { + // Log error mais on a déjà réussi l'upload principal + fmt.Printf("Warning: Failed to re-init B2 client for tag refs: %v\n", err) + return objectNames, nil // Return only the main object name } - - // Exposer le port 8080 par défaut - dockerfile.WriteString("EXPOSE 8080\n\n") - - // Commande par défaut - dockerfile.WriteString("CMD [\"sh\", \"-c\", \"echo 'Image construite avec succès à partir de multiples codebases'\"]\n") - - return dockerfile.String(), nil -} - -// BuildWithBufferInput construit une image Docker à partir de données en mémoire -func (s *BuildService) BuildWithBufferInput(ctx context.Context, dockerfile string, contextData map[string][]byte, tags []string) (*BuildResult, error) { - tmpDir, err := ioutil.TempDir("", "docker-build-buffer-") + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) if err != nil { - return nil, fmt.Errorf("impossible de créer un répertoire temporaire: %w", err) + fmt.Printf("Warning: Failed to get B2 bucket for tag refs: %v\n", err) + return objectNames, nil } - defer os.RemoveAll(tmpDir) - // Créer le Dockerfile - dockerfilePath := filepath.Join(tmpDir, "Dockerfile") - if err := ioutil.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil { - return nil, fmt.Errorf("erreur lors de la création du Dockerfile: %w", err) - } + for _, tag := range tags { + cleanTag := strings.ReplaceAll(tag, ":", "-") + cleanTag = strings.ReplaceAll(cleanTag, "/", "_") // Replace slashes too + tagFileName := fmt.Sprintf("%s.ref.txt", cleanTag) + tagPath := filepath.Join(s.b2Config.BasePath, tagFileName) - // Créer les fichiers de contexte - for filePath, content := range contextData { - fullPath := filepath.Join(tmpDir, filePath) + refContent := fmt.Sprintf("ImageID: %s\nTag: %s\nVersion: %s\nServiceName: %s\nMainObject: %s\n", + imageID, tag, version, serviceName, objectNames[0]) - // Créer les répertoires parents - dirPath := filepath.Dir(fullPath) - if err := os.MkdirAll(dirPath, 0755); err != nil { - return nil, fmt.Errorf("erreur lors de la création du répertoire %s: %w", dirPath, err) - } + refObj := bucket.Object(tagPath) + refWriter := refObj.NewWriter(ctx) - // Écrire le fichier - if err := ioutil.WriteFile(fullPath, content, 0644); err != nil { - return nil, fmt.Errorf("erreur lors de l'écriture du fichier %s: %w", filePath, err) + _, err = refWriter.Write([]byte(refContent)) + if err != nil { + refWriter.Close() + fmt.Printf("Warning: Failed to write B2 ref file for tag '%s' (%s): %v\n", tag, tagPath, err) + continue // Continue with other tags } + err = refWriter.Close() + if err != nil { + fmt.Printf("Warning: Failed to close B2 ref file for tag '%s' (%s): %v\n", tag, tagPath, err) + continue + } + objectNames = append(objectNames, tagPath) } - // Créer une spécification de build minimale - spec := &BuildSpec{ - Name: "buffer-build", - Version: time.Now().Format("20060102-150405"), - BuildConfig: BuildConfig{ - Tags: tags, - }, - } + return objectNames, nil +} - // Construire l'image - imageID, logs, err := s.buildWithDocker(ctx, tmpDir, dockerfilePath, spec) +// extractFromContainer copie un fichier/dossier depuis un conteneur temporaire +func (s *BuildService) extractFromContainer(ctx context.Context, imageID, containerPath string) ([]byte, error) { + // Créer un conteneur temporaire basé sur l'image + resp, err := s.dockerClient.ContainerCreate(ctx, &container.Config{Image: imageID}, nil, nil, nil, "") if err != nil { - return &BuildResult{ - Success: false, - Logs: logs, - ErrorMessage: err.Error(), - }, err + return nil, fmt.Errorf("erreur lors de la création du conteneur temporaire pour l'extraction: %w", err) } + containerID := resp.ID + defer s.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) // Cleanup - // Récupérer la taille de l'image - imageSize, _ := s.getImageSize(ctx, imageID) - - return &BuildResult{ - Success: true, - ImageID: imageID, - ImageSize: imageSize, - Logs: logs, - }, nil -} - -// GetImageInfo récupère des informations détaillées sur une image Docker -func (s *BuildService) GetImageInfo(ctx context.Context, imageID string) (*types.ImageInspect, error) { - inspect, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) + // Copier le fichier/dossier depuis le conteneur + readCloser, _, err := s.dockerClient.CopyFromContainer(ctx, containerID, containerPath) if err != nil { - return nil, err + return nil, fmt.Errorf("erreur lors de la copie depuis le conteneur '%s' (path: %s): %w", containerID, containerPath, err) } - return &inspect, nil -} - -// TagImage ajoute un tag à une image Docker -func (s *BuildService) TagImage(ctx context.Context, imageID string, tag string) error { - return s.dockerClient.ImageTag(ctx, imageID, tag) -} - -// PushImage pousse une image vers un registry Docker -func (s *BuildService) PushImage(ctx context.Context, tag string, auth string) (string, error) { - var authConfig interface{} + defer readCloser.Close() - // Si auth est fourni, le décoder - if auth != "" { - authJSON, err := base64.StdEncoding.DecodeString(auth) - if err != nil { - return "", fmt.Errorf("erreur lors du décodage de l'authentification: %w", err) - } + // Lire le contenu de l'archive tar retournée par CopyFromContainer + tarReader := tar.NewReader(readCloser) - if err := json.Unmarshal(authJSON, &authConfig); err != nil { - return "", fmt.Errorf("erreur lors du parsing de l'authentification: %w", err) - } + // On s'attend à un seul fichier (ou le premier fichier si c'est un dossier) + // Pour extraire un dossier complet, il faudrait itérer et recréer la structure + header, err := tarReader.Next() + if err == io.EOF { + return nil, fmt.Errorf("aucune donnée trouvée dans l'archive copiée depuis le conteneur (path: %s)", containerPath) } - - // Encoder l'authentification pour l'API Docker - encodedAuth, err := json.Marshal(authConfig) if err != nil { - return "", fmt.Errorf("erreur lors de l'encodage de l'authentification: %w", err) + return nil, fmt.Errorf("erreur lors de la lecture de l'en-tête tar depuis la copie du conteneur: %w", err) } - authStr := base64.URLEncoding.EncodeToString(encodedAuth) - // Options de push - options := image.PushOptions{ - RegistryAuth: authStr, + // Vérifier si c'est un fichier régulier + if header.Typeflag != tar.TypeReg { + // Si c'est un dossier, on pourrait vouloir lire le premier fichier ou échouer? + // Pour un binaire, on s'attend à un fichier. + // Tentative : si c'est un dossier, lire le contenu du premier fichier trouvé dedans? + // Ou simplement retourner une erreur si ce n'est pas un fichier régulier. + return nil, fmt.Errorf("le chemin extrait '%s' n'est pas un fichier régulier (type: %c)", containerPath, header.Typeflag) } - // Push l'image - pushResp, err := s.dockerClient.ImagePush(ctx, tag, options) + // Lire le contenu du fichier + fileData, err := io.ReadAll(tarReader) if err != nil { - return "", fmt.Errorf("erreur lors du push de l'image: %w", err) + return nil, fmt.Errorf("erreur lors de la lecture du contenu du fichier depuis l'archive tar: %w", err) } - defer pushResp.Close() - - // Lire les logs du push - var logs bytes.Buffer - decoder := json.NewDecoder(pushResp) - for { - var msg jsonmessage.JSONMessage - if err := decoder.Decode(&msg); err != nil { - if err == io.EOF { - break - } - return logs.String(), fmt.Errorf("erreur lors de la lecture des logs: %w", err) - } - - if msg.Error != nil { - return logs.String(), fmt.Errorf("erreur dans le push: %s", msg.Error.Message) - } - - if msg.Status != "" { - fmt.Fprintf(&logs, "%s: %s\n", time.Now().Format("15:04:05"), msg.Status) - } - if msg.Progress != nil { - fmt.Fprintf(&logs, "%s\n", msg.Progress.String()) - } - } - - return logs.String(), nil + return fileData, nil } -// CompressAndUploadArtifacts compresse les artefacts et les upload vers B2 -func (s *BuildService) CompressAndUploadArtifacts(ctx context.Context, artifacts map[string][]byte, name string) (string, error) { - if len(artifacts) == 0 { - return "", fmt.Errorf("aucun artefact à compresser") - } - - // Créer une archive tar.gz en mémoire - var buf bytes.Buffer - gzw := gzip.NewWriter(&buf) - tw := tar.NewWriter(gzw) +// generateRunYAML crée la structure pour *.run.yml (CORRIGÉ pour accepter projet parsé) +func (s *BuildService) generateRunYAML(ctx context.Context, spec *BuildSpec, result *BuildResult, runtimeEnv map[string]string, finalImageTags map[string][]string, composeProject *ComposeProject) (*RunYAML, error) { // Modifié: Prend *ComposeProject + runYAML := &RunYAML{ + Version: "1.0", + Services: make(map[string]RunService), + } + + if composeProject != nil { // Utiliser le projet parsé si fourni + // Base run.yml on the parsed compose file structure + for serviceName, service := range composeProject.Services { + // Skip build-only services? (Nécessite une logique/annotation pour identifier) + // Pour l'instant, on inclut tous les services définis. + // isBuildOnly := false // Logique à ajouter si nécessaire + // if isBuildOnly { continue } + + runService := RunService{ + Image: s.getImageRefForRun(serviceName, spec.RunConfigDef.ArtifactStorage, result, finalImageTags), + Command: service.Command, + Entrypoint: service.Entrypoint, + Environment: make(map[string]string), + Ports: service.Ports, // Directement []string maintenant + Volumes: service.Volumes, // Directement []string maintenant + Restart: service.Restart, + DependsOn: service.DependsOn, // Directement []string maintenant + } - // Ajouter chaque artefact - for path, data := range artifacts { - header := &tar.Header{ - Name: path, - Mode: 0644, - Size: int64(len(data)), - } + // Combine env vars: Global runtime env puis Service-specific + for k, v := range runtimeEnv { + runService.Environment[k] = v + } + if service.Environment != nil { + for k, vPtr := range service.Environment { + if vPtr != nil { + // NOTE: Pas d'interpolation ici ! Les valeurs sont littérales. + runService.Environment[k] = *vPtr + } else { + // Variable définie sans valeur (ex: FOO:) -> essayer l'env host? Mettre vide? + // Mettons vide pour l'instant pour la simplicité. + runService.Environment[k] = "" + } + } + } + // Copier d'autres champs si définis dans RunService (ex: HealthCheck, Labels) + // runService.HealthCheck = service.HealthCheck // Si RunService a un HealthCheck - if err := tw.WriteHeader(header); err != nil { - return "", fmt.Errorf("erreur lors de l'écriture de l'en-tête tar: %w", err) + runYAML.Services[serviceName] = runService } - if _, err := tw.Write(data); err != nil { - return "", fmt.Errorf("erreur lors de l'écriture des données tar: %w", err) + } else { + // Single service based on the main build spec name (non-compose build) + mainServiceName := spec.Name + // Vérifier si cette image existe (au cas où le build a échoué mais on génère quand même) + if _, ok := result.ImageIDs[mainServiceName]; !ok && spec.RunConfigDef.ArtifactStorage != "local" { + fmt.Printf("Warning: Image for main service '%s' not found in results, skipping run.yml generation for it.\n", mainServiceName) + // Retourner un run.yml vide ou une erreur? Retournons le runYAML potentiellement vide. + } else { + runService := RunService{ + Image: s.getImageRefForRun(mainServiceName, spec.RunConfigDef.ArtifactStorage, result, finalImageTags), + Environment: runtimeEnv, + Command: spec.RunConfigDef.Commands, // Utiliser les commandes globales définies + // Ajouter d'autres champs par défaut si nécessaire + } + runYAML.Services[mainServiceName] = runService } } - // Fermer les writers - if err := tw.Close(); err != nil { - return "", err - } - if err := gzw.Close(); err != nil { - return "", err + // Vérifier si aucun service n'a été ajouté (peut arriver si build compose échoue complètement) + if len(runYAML.Services) == 0 { + fmt.Println("Warning: No services could be added to run.yml.") + // Retourner une erreur ou un succès avec un fichier vide ? Succès vide pour l'instant. } - // Générer un nom pour l'archive - archiveName := fmt.Sprintf("%s-artifacts-%s.tar.gz", name, time.Now().Format("20060102-150405")) - - // Upload vers B2 - return s.UploadArtifactToB2(ctx, buf.Bytes(), archiveName) + return runYAML, nil } -// ExecuteInContainer exécute une commande dans un conteneur temporaire basé sur une image -func (s *BuildService) ExecuteInContainer(ctx context.Context, imageID string, cmd []string, env []string) (string, error) { - // Configuration du conteneur - config := &container.Config{ - Image: imageID, - Cmd: cmd, - Env: env, - Tty: false, - AttachStdout: true, - AttachStderr: true, - } - - // Créer le conteneur - resp, err := s.dockerClient.ContainerCreate(ctx, config, nil, nil, nil, "") - if err != nil { - return "", fmt.Errorf("erreur lors de la création du conteneur: %w", err) - } - containerID := resp.ID - - // Nettoyer le conteneur à la fin - defer s.dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{ - Force: true, - }) - - // Démarrer le conteneur - if err := s.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { - return "", fmt.Errorf("erreur lors du démarrage du conteneur: %w", err) - } - - // Attendre que le conteneur termine - statusCh, errCh := s.dockerClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) - - var status container.WaitResponse - select { - case err := <-errCh: - return "", fmt.Errorf("erreur lors de l'attente du conteneur: %w", err) - case status = <-statusCh: - // Continuer - } - - // Récupérer les logs - out, err := s.dockerClient.ContainerLogs(ctx, containerID, container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - }) - if err != nil { - return "", fmt.Errorf("erreur lors de la récupération des logs: %w", err) - } - defer out.Close() - - // Lire les logs - var output bytes.Buffer - _, err = stdcopy.StdCopy(&output, &output, out) - if err != nil { - return "", fmt.Errorf("erreur lors de la lecture des logs: %w", err) - } - - // Vérifier le code de sortie - if status.StatusCode != 0 { - return output.String(), fmt.Errorf("commande terminée avec code de sortie %d", status.StatusCode) - } - - return output.String(), nil -} - -// GenerateMultistageDockerfile génère un Dockerfile multistage pour combiner plusieurs codebases -func (s *BuildService) GenerateMultistageDockerfile(spec *BuildSpec) (string, error) { - if len(spec.Codebases) == 0 { - return "", fmt.Errorf("aucune codebase spécifiée") - } - - var dockerfile strings.Builder - - // Détecter les types de projets dans chaque codebase - // Cette fonction simplifiée détecte Go, Node.js, Python, et Java - projectTypes := make(map[string]string) - for _, codebase := range spec.Codebases { - // Dans une implémentation réelle, cette détection serait plus sophistiquée - if codebase.Path != "" && codebase.Path != "." { - projectTypes[codebase.Name] = "unknown" // Par défaut - } else { - projectTypes[codebase.Name] = "unknown" // Par défaut +// getImageRefForRun détermine la référence d'image à utiliser dans run.yml +func (s *BuildService) getImageRefForRun(serviceName, storageType string, result *BuildResult, finalImageTags map[string][]string) string { + switch storageType { + case "local": + if path, ok := result.LocalImagePaths[serviceName]; ok && path != "" { + // Retourner seulement le nom du fichier .tar + return filepath.Base(path) } - } - - // Générer les étapes de build pour chaque codebase - for _, codebase := range spec.Codebases { - dockerfile.WriteString(fmt.Sprintf("# Étape de build pour %s\n", codebase.Name)) - - // Déterminer l'image de base selon le type de projet - switch projectTypes[codebase.Name] { - case "go": - dockerfile.WriteString(fmt.Sprintf("FROM golang:1.19-alpine AS %s-builder\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) - dockerfile.WriteString("RUN go build -o app .\n\n") - case "nodejs": - dockerfile.WriteString(fmt.Sprintf("FROM node:18-alpine AS %s-builder\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) - dockerfile.WriteString("RUN npm ci && npm run build\n\n") - case "python": - dockerfile.WriteString(fmt.Sprintf("FROM python:3.10-slim AS %s-builder\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) - dockerfile.WriteString("RUN pip install -r requirements.txt && python setup.py build\n\n") - case "java": - dockerfile.WriteString(fmt.Sprintf("FROM maven:3.8-openjdk-17 AS %s-builder\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n", codebase.Name)) - dockerfile.WriteString("RUN mvn package -DskipTests\n\n") - default: - // Base générique pour les projets inconnus - dockerfile.WriteString(fmt.Sprintf("FROM alpine:latest AS %s-builder\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("WORKDIR /build/%s\n", codebase.Name)) - dockerfile.WriteString(fmt.Sprintf("COPY %s/ .\n\n", codebase.Name)) + // Fallback si chemin non trouvé + fmt.Printf("Warning: Local image path not found for service '%s' in build result.\n", serviceName) + return fmt.Sprintf("local:%s_image_not_found.tar", serviceName) + + case "docker": + // Utiliser le premier tag trouvé pour ce service + if tags, ok := finalImageTags[serviceName]; ok && len(tags) > 0 && tags[0] != "" { + return tags[0] // Utilise le premier tag appliqué } - } - - // Image finale - baseImage := spec.BuildConfig.BaseImage - if baseImage == "" { - baseImage = "alpine:latest" - } - - dockerfile.WriteString("# Image finale\n") - dockerfile.WriteString(fmt.Sprintf("FROM %s\n", baseImage)) - dockerfile.WriteString("WORKDIR /app\n\n") + // Fallback si aucun tag trouvé (ne devrait pas arriver si build a réussi et taggé) + fmt.Printf("Warning: No Docker tags found for service '%s' in finalImageTags map.\n", serviceName) + // En dernier recours, utiliser l'ID si disponible ? Ou un tag par défaut ? Utilisons un tag par défaut. + if result.ImageIDs != nil { + if imgID, ok := result.ImageIDs[serviceName]; ok && imgID != "" { + fmt.Printf("Warning: Falling back to default tag for service '%s' as no specific tags were found.\n", serviceName) + // Construire un tag par défaut plausible (peut nécessiter le nom du projet) + // Ceci est un fallback, la logique de tagging dans Build() devrait être la source principale. + return fmt.Sprintf("%s:latest", serviceName) // Simple fallback + } + } + return fmt.Sprintf("docker:%s_image_or_tag_not_found", serviceName) - // Copier les artefacts de chaque étape de build - for _, codebase := range spec.Codebases { - switch projectTypes[codebase.Name] { - case "go": - dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/app /app/%s/\n", - codebase.Name, codebase.Name, codebase.Name)) - case "nodejs": - dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/dist /app/%s/\n", - codebase.Name, codebase.Name, codebase.Name)) - case "python": - dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/build /app/%s/\n", - codebase.Name, codebase.Name, codebase.Name)) - case "java": - dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/target/*.jar /app/%s/\n", - codebase.Name, codebase.Name, codebase.Name)) - default: - dockerfile.WriteString(fmt.Sprintf("COPY --from=%s-builder /build/%s/ /app/%s/\n", - codebase.Name, codebase.Name, codebase.Name)) + default: // Cas inconnu ou "" + fmt.Printf("Warning: Unknown artifact storage type '%s'. Falling back to default behavior.\n", storageType) + // Comportement par défaut : essayer tag docker, puis id, puis fallback + if tags, ok := finalImageTags[serviceName]; ok && len(tags) > 0 && tags[0] != "" { + return tags[0] + } + if result.ImageIDs != nil { + if imgID, ok := result.ImageIDs[serviceName]; ok && imgID != "" { + return imgID // Retourne l'ID si pas de tag + } } + return fmt.Sprintf("unknown_storage:%s_not_found", serviceName) } - - // Exposer le port 8080 par défaut - dockerfile.WriteString("\nEXPOSE 8080\n\n") - - // Commande par défaut - dockerfile.WriteString("CMD [\"sh\", \"-c\", \"echo 'Image multi-stage construite avec succès'\"]\n") - - return dockerfile.String(), nil -} - -// SetupDockerIgnore crée un fichier .dockerignore pour exclure les fichiers inutiles -func (s *BuildService) SetupDockerIgnore(buildDir string) error { - dockerignore := `# Fichiers de système -.DS_Store -Thumbs.db - -# Répertoires de dépendances -**/node_modules -**/vendor -**/.venv -**/env -**/venv -**/__pycache__ - -# Fichiers de build -**/dist -**/build -**/out -**/bin -**/target -**/.next -**/.nuxt - -# Fichiers de développement -**/.git -**/.github -**/.vscode -**/.idea -**/*.log -**/*.swp -**/*.swo -**/coverage -**/tmp - -# Fichiers de configuration Docker -Dockerfile* -docker-compose* -.dockerignore - -# Fichiers de test -**/*_test.go -**/test -**/tests -**/*.spec.js -**/*.test.js -` - - return os.WriteFile(filepath.Join(buildDir, ".dockerignore"), []byte(dockerignore), 0644) } -// GenerateImageReport crée un rapport détaillé sur une image Docker -func (s *BuildService) GenerateImageReport(ctx context.Context, imageID string) (map[string]interface{}, error) { - report := make(map[string]interface{}) - - // Informations générales sur l'image - inspect, _, err := s.dockerClient.ImageInspectWithRaw(ctx, imageID) - if err != nil { - return nil, fmt.Errorf("erreur lors de l'inspection de l'image: %w", err) - } - - report["image_id"] = inspect.ID - report["created"] = inspect.Created - report["size"] = inspect.Size - report["virtual_size"] = inspect.VirtualSize - report["tags"] = inspect.RepoTags - report["labels"] = inspect.Config.Labels - report["entrypoint"] = inspect.Config.Entrypoint - report["cmd"] = inspect.Config.Cmd - report["env"] = inspect.Config.Env - report["exposed_ports"] = inspect.Config.ExposedPorts - report["os"] = inspect.Os - report["architecture"] = inspect.Architecture - - // Récupérer l'historique de l'image - history, err := s.dockerClient.ImageHistory(ctx, imageID) - if err != nil { - return report, fmt.Errorf("erreur lors de la récupération de l'historique: %w", err) - } - - historyInfo := make([]map[string]interface{}, 0, len(history)) - for _, layer := range history { - historyInfo = append(historyInfo, map[string]interface{}{ - "created": layer.Created, - "created_by": layer.CreatedBy, - "size": layer.Size, - "comment": layer.Comment, - "tags": layer.Tags, - }) - } - report["history"] = historyInfo - - return report, nil +// --- Other Existing Functions (Potentially useful, keep for now) --- +// CreateContainer, RunContainer, ExportContainer, Cleanup, UploadArtifactToB2, +// DownloadImageFromB2, BuildWithMultipleCodebases, SaveDockerImageToBuffer, +// CreateDockerfileFromMultipleCodebases, BuildWithBufferInput, GetImageInfo, +// TagImage, PushImage, CompressAndUploadArtifacts, ExecuteInContainer, +// GenerateMultistageDockerfile, SetupDockerIgnore, GenerateImageReport + +// Note: Some functions like BuildWithMultipleCodebases might be redundant now +// that the main Build function handles codebases properly. Review and refactor/remove later. +// ExecuteInContainer might be useful for BuildSteps binary extraction, but needs refinement. +// Added import for "github.com/docker/docker/pkg/term" for pull progress + +// Placeholder pour l'implémentation du SecretFetcher si non fourni +type DummySecretFetcher struct{} + +func (d *DummySecretFetcher) GetSecret(ctx context.Context, source string) (string, error) { + fmt.Printf("Warning: Using DummySecretFetcher. Secret '%s' not actually fetched.\n", source) + // Retourner une valeur placeholder ou une erreur selon le comportement souhaité + return fmt.Sprintf("dummy-secret-for-%s", source), nil + // Ou: return "", fmt.Errorf("secret fetcher not implemented") } diff --git a/bx/build/detector.go b/bx/build/detector.go new file mode 100644 index 0000000..c9044ee --- /dev/null +++ b/bx/build/detector.go @@ -0,0 +1,158 @@ +package build + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +var ( + ErrAmbiguousEcosystem = errors.New("multiple incompatible major ecosystems detected (e.g., Go and Rust). Cannot auto-resolve") + ErrNoEcosystemFound = errors.New("no supported ecosystem found (e.g., go.mod, package.json, Cargo.toml) at project root") + ErrNoTemplateFound = errors.New("no Dockerfile template found for the detected ecosystem") +) + +// DetectedEcosystem holds language/ecosystem detection details +// Compatible with extensible language addition. +type DetectedEcosystem struct { + Language string + Ecosystem string + PackageManager string + RootPath string + MainMarkerFile string +} + +type detectionCandidate struct { + ecosystem DetectedEcosystem + priority int +} + +// DetectEcosystem returns the main detected ecosystem in a project directory +func DetectEcosystem(codebasePath string) (*DetectedEcosystem, error) { + absPath, err := filepath.Abs(codebasePath) + if err != nil { + return nil, fmt.Errorf("cannot resolve absolute path for %s: %w", codebasePath, err) + } + + info, err := os.Stat(absPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("codebase path %s does not exist: %w", absPath, err) + } + return nil, fmt.Errorf("error checking path %s: %w", absPath, err) + } + if !info.IsDir() { + return nil, fmt.Errorf("codebase path %s is not a directory", absPath) + } + + primaryMarkers := loadPrimaryMarkers() + secondaryMarkers := loadSecondaryMarkers() + + entries, err := os.ReadDir(absPath) + if err != nil { + return nil, fmt.Errorf("cannot read directory %s: %w", absPath, err) + } + + detected, err := scanMarkers(absPath, entries, primaryMarkers) + if err != nil { + return nil, err + } + + postDetectionTweaks(absPath, entries, detected, secondaryMarkers) + fmt.Printf("Detected ecosystem: %s (%s) using %s in %s\n", detected.Language, detected.Ecosystem, detected.PackageManager, detected.RootPath) + return detected, nil +} + +func loadPrimaryMarkers() map[string]detectionCandidate { + return map[string]detectionCandidate{ + "go.work": {DetectedEcosystem{"Go", "Workspaces", "go", "", ""}, 10}, + "go.mod": {DetectedEcosystem{"Go", "Modules", "go", "", ""}, 9}, + "Cargo.toml": {DetectedEcosystem{"Rust", "Cargo", "cargo", "", ""}, 9}, + "package.json": {DetectedEcosystem{"JavaScript", "Node", "npm", "", ""}, 8}, + "pom.xml": {DetectedEcosystem{"Java", "Maven", "mvn", "", ""}, 9}, + "build.gradle": {DetectedEcosystem{"Java", "Gradle", "gradle", "", ""}, 9}, + "build.gradle.kts": {DetectedEcosystem{"Java", "Gradle", "gradle", "", ""}, 9}, + "requirements.txt": {DetectedEcosystem{"Python", "Pip", "pip", "", ""}, 8}, + "pyproject.toml": {DetectedEcosystem{"Python", "Poetry/Pip", "pip", "", ""}, 9}, + "composer.json": {DetectedEcosystem{"PHP", "Composer", "composer", "", ""}, 9}, + "Gemfile": {DetectedEcosystem{"Ruby", "Bundler", "bundle", "", ""}, 9}, + "*.csproj": {DetectedEcosystem{"C#", "MSBuild", "dotnet", "", ""}, 9}, + "Package.swift": {DetectedEcosystem{"Swift", "SwiftPM", "swift", "", ""}, 9}, + "build.gradle.kts.kts": {DetectedEcosystem{"Kotlin", "Gradle", "gradle", "", ""}, 9}, + } +} + +func loadSecondaryMarkers() map[string]struct{ PackageManager, Ecosystem string } { + return map[string]struct{ PackageManager, Ecosystem string }{ + "pnpm-lock.yaml": {"pnpm", "PNPM"}, + "yarn.lock": {"yarn", "Yarn"}, + } +} + +func scanMarkers(path string, entries []os.DirEntry, primary map[string]detectionCandidate) (*DetectedEcosystem, error) { + highestPriority := -1 + var detected *DetectedEcosystem + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if strings.Contains(name, ".csproj") { + if candidate, ok := primary["*.csproj"]; ok { + if detected != nil && detected.Language != candidate.ecosystem.Language { + return nil, fmt.Errorf("%w: detected %s (%s) and %s (%s)", ErrAmbiguousEcosystem, detected.MainMarkerFile, detected.Language, name, candidate.ecosystem.Language) + } + if candidate.priority > highestPriority { + highestPriority = candidate.priority + d := candidate.ecosystem + d.RootPath = path + d.MainMarkerFile = name + detected = &d + } + } + continue + } + if candidate, ok := primary[name]; ok { + if detected != nil && detected.Language != candidate.ecosystem.Language { + return nil, fmt.Errorf("%w: detected %s (%s) and %s (%s)", ErrAmbiguousEcosystem, detected.MainMarkerFile, detected.Language, name, candidate.ecosystem.Language) + } + if candidate.priority > highestPriority { + highestPriority = candidate.priority + d := candidate.ecosystem + d.RootPath = path + d.MainMarkerFile = name + detected = &d + } + } + } + + if detected == nil { + return nil, ErrNoEcosystemFound + } + return detected, nil +} + +func postDetectionTweaks(path string, entries []os.DirEntry, detected *DetectedEcosystem, secondary map[string]struct{ PackageManager, Ecosystem string }) { + if detected.MainMarkerFile == "package.json" { + bestLock := -1 + lockPriority := map[string]int{"pnpm-lock.yaml": 2, "yarn.lock": 1} + for _, entry := range entries { + name := entry.Name() + if val, ok := secondary[name]; ok && lockPriority[name] > bestLock { + bestLock = lockPriority[name] + detected.PackageManager = val.PackageManager + detected.Ecosystem = val.Ecosystem + } + } + } + + if detected.MainMarkerFile == "pyproject.toml" { + data, err := os.ReadFile(filepath.Join(path, "pyproject.toml")) + if err == nil && strings.Contains(string(data), "[tool.poetry]") { + detected.Ecosystem = "Poetry" + } + } +} diff --git a/bx/build/socket.go b/bx/build/socket.go new file mode 100644 index 0000000..145d978 --- /dev/null +++ b/bx/build/socket.go @@ -0,0 +1,462 @@ +package build + +import ( + // ... autres imports ... + "context" + "encoding/json" + "fmt" + "io" + "log" // Pour les logs internes + "os" + "path/filepath" + "strings" + "sync" + "time" + + // Importer le package socket (ajuster le chemin si nécessaire) + "anexis/socket" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/jsonmessage" // Nécessaire pour le writer de logs + "github.com/moby/go-archive" + // ... +) + +// Assurer que BuildService implémente les interfaces requises par le serveur socket +var _ socket.BuildTriggerer = (*BuildService)(nil) +var _ socket.SecretFetcher = (*BuildService)(nil) + +// --- Implémentation de socket.SecretFetcher --- + +// GetSecret récupère un secret en utilisant le fetcher configuré dans BuildService. +// Ceci suppose que vous avez déjà un moyen de récupérer les secrets DANS BuildService. +// Si ce n'est pas le cas, vous devrez adapter cette partie. + + +// --- Implémentation de socket.BuildTriggerer --- + +// logNotifierWriter est un io.Writer qui envoie les données écrites au BuildNotifier. +type logNotifierWriter struct { + buildID string + stream string // "stdout" or "stderr" + notifier socket.BuildNotifier + mu sync.Mutex // Protéger les appels concurrents potentiels à Write +} + +func newLogNotifierWriter(buildID string, stream string, notifier socket.BuildNotifier) *logNotifierWriter { + return &logNotifierWriter{ + buildID: buildID, + stream: stream, + notifier: notifier, + } +} + +func (lnw *logNotifierWriter) Write(p []byte) (n int, err error) { + if lnw.notifier == nil { + return len(p), nil // Ne rien faire si pas de notifier + } + lnw.mu.Lock() + defer lnw.mu.Unlock() + // Envoyer le contenu comme un chunk de log + // Convertir les bytes en string. Peut être optimisé si de très gros chunks sont attendus. + content := string(p) + lnw.notifier.NotifyLog(lnw.buildID, lnw.stream, content) + return len(p), nil +} + + +// StartBuildAsync lance un build en arrière-plan et notifie via le notifier. +func (s *BuildService) StartBuildAsync(ctx context.Context, buildID string, buildSpecYAML string, notifier socket.BuildNotifier) error { + log.Printf("[BuildID: %s] Received async build request.\n", buildID) + + // 1. Parser le BuildSpec depuis le YAML reçu + // Utiliser le format .yaml par défaut car c'est ce qu'on a défini dans le payload + spec, err := LoadBuildSpecFromBytes([]byte(buildSpecYAML), ".yaml") + if err != nil { + log.Printf("[BuildID: %s] Error parsing BuildSpec YAML: %v\n", buildID, err) + // Notifier immédiatement l'échec de démarrage + go notifier.NotifyStatus(buildID, "failure", "", fmt.Errorf("invalid build spec: %w", err), nil) + return fmt.Errorf("invalid build spec: %w", err) // Retourner l'erreur au serveur socket + } + log.Printf("[BuildID: %s] Parsed BuildSpec for '%s' version '%s'.\n", buildID, spec.Name, spec.Version) + + // 2. Lancer la logique de build réelle dans une goroutine + go s.runBuildLogic(ctx, buildID, spec, notifier) + + // 3. Retourner nil immédiatement pour indiquer que la tâche a été acceptée + log.Printf("[BuildID: %s] Build logic started in background.\n", buildID) + return nil +} + + +// runBuildLogic contient la logique de build principale, adaptée pour les notifications. +// ATTENTION: Cette fonction est maintenant longue et complexe. Envisager de la découper. +func (s *BuildService) runBuildLogic(ctx context.Context, buildID string, spec *BuildSpec, notifier socket.BuildNotifier) { + startTime := time.Now() + var buildErr error + var finalStatus string = "success" // Statut par défaut + var artifactRef string = "" // Référence de l'artefact final + + // Créer des writers pour capturer stdout/stderr et les envoyer au notifier + stdoutNotifier := newLogNotifierWriter(buildID, "stdout", notifier) + // stderrNotifier := newLogNotifierWriter(buildID, "stderr", notifier) // Peut être utile plus tard + + // Créer un logger dédié pour ce build qui écrit vers le notifier + buildLogger := log.New(stdoutNotifier, fmt.Sprintf("[%s] ", buildID), 0) // Pas de flags de date/heure par défaut + + // S'assurer que le statut final est envoyé même en cas de panic + defer func() { + duration := time.Since(startTime).Seconds() + if r := recover(); r != nil { + buildLogger.Printf("PANIC recovered during build: %v\n", r) + buildErr = fmt.Errorf("panic during build: %v", r) + finalStatus = "failure" + } + buildLogger.Printf("Build finished with status: %s (Error: %v)\n", finalStatus, buildErr) + notifier.NotifyStatus(buildID, finalStatus, artifactRef, buildErr, &duration) + }() + + + // --- Logique de Build (adaptée de Build()) --- + buildLogger.Println("Starting build process...") + notifier.NotifyStatus(buildID, "starting", "", nil, nil) // Statut initial + + // Utiliser un lock spécifique au build si BuildService a des champs partagés modifiables (ici, juste pour l'exemple) + // s.mutex.Lock() + // defer s.mutex.Unlock() // Attention à la durée du lock + + result := &BuildResult{ // Utiliser un result local pour stocker les infos internes + Artifacts: make(map[string][]byte), + ImageIDs: make(map[string]string), + ImageSizes: make(map[string]int64), + LocalImagePaths: make(map[string]string), + ServiceOutputs: make(map[string]ServiceOutput), + } + + // --- 1. Setup Build Environment --- + // Utiliser buildID pour un chemin unique + buildDir := filepath.Join(s.workDir, buildID) + if err := os.MkdirAll(buildDir, 0755); err != nil { + buildErr = fmt.Errorf("cannot create build directory '%s': %w", buildDir, err) + finalStatus = "failure" + return // Sortir après avoir mis à jour buildErr (defer s'occupera de notifier) + } + // Nettoyer seulement si succès et pas sortie locale SANS chemin spécifique + shouldCleanup := true + defer func() { + if shouldCleanup && buildErr == nil { // Nettoyer si succès + if !(spec.BuildConfig.OutputTarget == "local" && spec.BuildConfig.LocalPath == "") { + buildLogger.Printf("Cleaning up build directory: %s\n", buildDir) + os.RemoveAll(buildDir) + } else { + buildLogger.Printf("Keeping build directory for local output: %s\n", buildDir) + } + } else if buildErr != nil { + buildLogger.Printf("Keeping build directory due to error: %s\n", buildDir) + } + }() + buildLogger.Printf("Using build directory: %s\n", buildDir) + notifier.NotifyStatus(buildID, "preparing_env", "", nil, nil) + + // --- 2. Load Environment Variables --- + mergedEnv := make(map[string]string) + // Copier/Adapter la logique de chargement des EnvFiles et Env ici... + // Utiliser buildLogger.Printf pour les warnings/infos + buildLogger.Printf("Loading environment variables...\n") + // ... (logique de chargement de godotenv, etc.) ... + for k, v := range spec.Env { // Exemple simplifié + mergedEnv[k] = v + } + buildLogger.Printf("Loaded %d environment variables.\n", len(mergedEnv)) + + + // --- 3. Fetch Secrets --- + runtimeSecrets := make(map[string]string) + if s.secretFetcher != nil && len(spec.Secrets) > 0 { + buildLogger.Println("Fetching secrets...") + notifier.NotifyStatus(buildID, "fetching_secrets", "", nil, nil) + for _, secretSpec := range spec.Secrets { + secretValue, err := s.GetSecret(ctx, secretSpec.Source) // Utilise la méthode locale + if err != nil { + buildErr = fmt.Errorf("failed to fetch secret '%s' (source: %s): %w", secretSpec.Name, secretSpec.Source, err) + finalStatus = "failure" + return + } + runtimeSecrets[secretSpec.Name] = secretValue + // Ne pas logger la valeur du secret ! + buildLogger.Printf("Secret '%s' fetched successfully.\n", secretSpec.Name) + } + } + finalRuntimeEnv := make(map[string]string) + for k, v := range mergedEnv { finalRuntimeEnv[k] = v } + for k, v := range runtimeSecrets { finalRuntimeEnv[k] = v } + + + // --- 4. Download Resources --- + // Adapter la logique de téléchargement ici... Utiliser buildLogger. + notifier.NotifyStatus(buildID, "downloading_resources", "", nil, nil) + buildLogger.Println("Downloading resources...") + // ... (boucle sur spec.Resources, appel s.downloadFile, s.extractArchive...) ... + // En cas d'erreur, assigner buildErr et retourner + + + // --- 5. Prepare Codebases --- + notifier.NotifyStatus(buildID, "fetching_codebases", "", nil, nil) + buildLogger.Println("Fetching codebases...") + codebaseMap := make(map[string]CodebaseConfig) + for _, codebase := range spec.Codebases { + // ... (logique pour déterminer destDir) ... + destDir := filepath.Join(buildDir, codebase.Name) // Simplifié + buildLogger.Printf("Fetching codebase '%s' into %s\n", codebase.Name, destDir) + if err := s.fetchCodebase(ctx, codebase, destDir); err != nil { + buildErr = fmt.Errorf("failed to fetch codebase '%s': %w", codebase.Name, err) + finalStatus = "failure" + return + } + codebaseMap[codebase.Name] = codebase + } + + // --- 6. Execute Build Steps (si implémenté) --- + // Adapter la logique des BuildSteps ici... Utiliser buildLogger. + // ... + + + // --- 7. Main Build Execution --- + notifier.NotifyStatus(buildID, "building_image", "", nil, nil) + buildLogger.Println("Starting main build execution...") + // Ici, on doit passer le `stdoutNotifier` aux fonctions de build Docker + + if spec.BuildConfig.ComposeFile != "" { + // --- 7a. Build using Docker Compose --- + buildLogger.Printf("Building using Compose file: %s\n", spec.BuildConfig.ComposeFile) + // ... (charger le projet compose comme avant, mais passer stdoutNotifier aux appels build) ... + // buildErrs := s.buildComposeProject(ctx, buildDir, composeProject, spec, result, buildLogger) // Adapter buildComposeProject + buildErr = fmt.Errorf("compose build via socket not fully adapted yet") // Placeholder + finalStatus = "failure" + return + } else { + // --- 7b. Build using Dockerfile --- + dockerfilePath, buildContextDir, err := s.findDockerfile(buildDir, spec) + if err != nil { + buildErr = err + finalStatus = "failure" + return + } + buildLogger.Printf("Building with Dockerfile: %s (Context: %s)\n", dockerfilePath, buildContextDir) + + // *** Modifier buildSingleImage pour accepter un io.Writer pour les logs *** + imageID, err := s.buildSingleImageWithLogs(ctx, buildContextDir, dockerfilePath, spec, stdoutNotifier) // Nouvelle fonction + if err != nil { + buildErr = fmt.Errorf("docker build failed: %w", err) + finalStatus = "failure" + return + } + + // Stocker le résultat + result.ImageID = imageID + imageSize, _ := s.getImageSize(ctx, imageID) // Ignorer l'erreur de taille pour l'instant + result.ImageSize = imageSize + mainServiceName := spec.Name + result.ImageIDs[mainServiceName] = imageID + result.ImageSizes[mainServiceName] = imageSize + result.ServiceOutputs[mainServiceName] = ServiceOutput{ImageID: imageID, ImageSize: imageSize} + buildLogger.Printf("Dockerfile build successful. ImageID: %s\n", imageID) + } + + + // --- 8. Handle Build Outputs --- + notifier.NotifyStatus(buildID, "saving_artifacts", "", nil, nil) + buildLogger.Println("Handling build outputs...") + // ... (logique de tagging d'image comme avant) ... + finalImageTags := make(map[string][]string) // Recréer cette map pour le run.yml + // ... (appliquer les tags) ... + + // Adapter la logique de OutputTarget + outputBasePath := buildDir // Base par défaut + if spec.BuildConfig.OutputTarget == "local" && spec.BuildConfig.LocalPath != "" { + outputBasePath = spec.BuildConfig.LocalPath // Logique inchangée + os.MkdirAll(outputBasePath, 0755) // Créer si besoin + } + + buildLogger.Printf("Output target: %s\n", spec.BuildConfig.OutputTarget) + switch spec.BuildConfig.OutputTarget { + case "b2": + // ... (logique exportAndUploadImage) ... + // artifactRef = ... (chemin B2 principal) + artifactRef = "b2://not/implemented/yet" // Placeholder + case "local": + for serviceName, serviceOutput := range result.ServiceOutputs { + imageFileName := fmt.Sprintf("%s_%s.tar", spec.Name, serviceName) + localImagePath := filepath.Join(outputBasePath, imageFileName) + buildLogger.Printf("Saving image for service '%s' locally to %s...\n", serviceName, localImagePath) + err := s.saveImageLocally(ctx, serviceOutput.ImageID, localImagePath) + if err != nil { + buildErr = fmt.Errorf("failed to save image '%s' locally: %w", serviceName, err) + finalStatus = "failure" + return + } + result.LocalImagePaths[serviceName] = localImagePath + if serviceName == spec.Name { // Assigner la ref de l'artefact principal + artifactRef = localImagePath // Chemin absolu ici + } + } + // Si sortie locale sans chemin spécifique, ne pas nettoyer le buildDir + if spec.BuildConfig.LocalPath == "" { + shouldCleanup = false + } + + case "docker": + default: + // Les images sont dans le daemon, utiliser le tag comme référence + tags := finalImageTags[spec.Name] + if len(tags) > 0 { + artifactRef = tags[0] // Premier tag + } else { + artifactRef = result.ImageID // Fallback sur l'ID + } + buildLogger.Printf("Images available in local Docker daemon. Artifact ref: %s\n", artifactRef) + + } + if buildErr != nil { return } // Vérifier après la gestion des sorties + + + // --- 9. Generate *.run.yml (si demandé) --- + if spec.RunConfigDef.Generate { + buildLogger.Println("Generating *.run.yml file...") + // ... (Logique de generateRunYAML comme avant, mais charger le projet compose ici si nécessaire) ... + // Le chemin de sortie doit être dans outputBasePath + runConfigPath := filepath.Join(outputBasePath, fmt.Sprintf("%s-%s.run.yml", spec.Name, spec.Version)) + // ... (générer et écrire le fichier) ... + // Si succès, on pourrait ajouter le chemin run.yml à l'artifactRef ou un message de statut ? + } + + buildLogger.Println("Build process completed successfully.") + // Le defer s'occupera d'envoyer le statut final "success" +} + + +// findDockerfile (helper extrait de Build) +func (s *BuildService) findDockerfile(buildDir string, spec *BuildSpec) (dockerfilePath, buildContextDir string, err error) { + buildContextDir = buildDir // Default + + if spec.BuildConfig.Dockerfile != "" { + if strings.Contains(spec.BuildConfig.Dockerfile, "\n") { + dockerfilePath = filepath.Join(buildDir, "Dockerfile.inline") + if err = os.WriteFile(dockerfilePath, []byte(spec.BuildConfig.Dockerfile), 0644); err != nil { + err = fmt.Errorf("failed to write inline Dockerfile: %w", err) + return + } + } else { + dockerfilePath = filepath.Join(buildDir, spec.BuildConfig.Dockerfile) + buildContextDir = filepath.Dir(dockerfilePath) // Ajuster le contexte + } + } else { + // Auto-detect + dfPath := filepath.Join(buildDir, "Dockerfile") + if _, statErr := os.Stat(dfPath); statErr == nil { + dockerfilePath = dfPath + } else if len(spec.Codebases) > 0 { // Fallback sur la première codebase + firstCodebaseDir := filepath.Join(buildDir, spec.Codebases[0].Name) + dfPath = filepath.Join(firstCodebaseDir, "Dockerfile") + if _, statErr := os.Stat(dfPath); statErr == nil { + dockerfilePath = dfPath + buildContextDir = firstCodebaseDir + } + } + } + + if dockerfilePath == "" { + err = fmt.Errorf("no Dockerfile specified or found") + return + } + if _, statErr := os.Stat(dockerfilePath); os.IsNotExist(statErr) { + err = fmt.Errorf("specified or detected Dockerfile does not exist: %s", dockerfilePath) + return + } + + return filepath.Clean(dockerfilePath), filepath.Clean(buildContextDir), nil +} + +// buildSingleImageWithLogs est la version de buildSingleImage qui accepte un io.Writer pour les logs. +func (s *BuildService) buildSingleImageWithLogs(ctx context.Context, buildContextDir string, dockerfilePath string, spec *BuildSpec, logWriter io.Writer) (string, error) { + buildContextTar, err := archive.TarWithOptions(buildContextDir, &archive.TarOptions{}) + if err != nil { + fmt.Fprintf(logWriter, "ERROR creating build context tar: %v\n", err) + return "", fmt.Errorf("error creating context tar for '%s': %w", buildContextDir, err) + } + defer buildContextTar.Close() + + buildOptions := types.ImageBuildOptions{ + Dockerfile: filepath.Base(dockerfilePath), + Tags: spec.BuildConfig.Tags, + Remove: true, + ForceRemove: true, + NoCache: spec.BuildConfig.NoCache, + BuildArgs: make(map[string]*string), + PullParent: spec.BuildConfig.Pull, + Version: types.BuilderBuildKit, // Préférer BuildKit + Target: spec.BuildConfig.Target, + // Platforms: spec.BuildConfig.Platforms, // Ajouter si besoin + } + if !spec.BuildConfig.BuildKit { buildOptions.Version = types.BuilderV1 } + for k, v := range spec.BuildConfig.Args { value := v; buildOptions.BuildArgs[k] = &value } + + fmt.Fprintf(logWriter, "Starting Docker build (Dockerfile: %s, Context: %s)...\n", buildOptions.Dockerfile, buildContextDir) + buildResponse, err := s.dockerClient.ImageBuild(ctx, buildContextTar, buildOptions) + // ... (gestion fallback legacy builder si besoin) ... + if err != nil { + fmt.Fprintf(logWriter, "ERROR starting Docker build: %v\n", err) + return "", fmt.Errorf("error starting Docker build: %w", err) + } + defer buildResponse.Body.Close() + + // Streamer la sortie JSON vers le logWriter fourni + var imageID string + err = jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, logWriter, 0, false, func(msg jsonmessage.JSONMessage) { + // Essayer d'extraire l'ID de l'image depuis les messages "Successfully built" ou Aux + if strings.Contains(msg.Stream, "Successfully built ") { + parts := strings.Fields(msg.Stream) + if len(parts) >= 3 && parts[0] == "Successfully" && parts[1] == "built" { + id := strings.TrimPrefix(parts[2], "sha256:") + if id != "" { imageID = id } + } + } + if msg.Aux != nil { + var auxMsg struct { ID string `json:"ID"` } + if json.Unmarshal(*msg.Aux, &auxMsg) == nil && auxMsg.ID != "" { + id := strings.TrimPrefix(auxMsg.ID, "sha256:") + if id != "" { imageID = id } // Préférer l'ID de Aux + } + } + }) + + if err != nil { + fmt.Fprintf(logWriter, "ERROR streaming build logs: %v\n", err) + // Continuer pour voir si on a quand même eu un ID + } + + if imageID == "" { + // Essayer de récupérer l'ID via le tag si possible + if len(buildOptions.Tags) > 0 { + inspected, inspectErr := s.getImageInfoByTag(ctx, buildOptions.Tags[0]) + if inspectErr == nil { + imageID = inspected.ID + fmt.Fprintf(logWriter, "Retrieved Image ID via tag inspection: %s\n", imageID) + } else { + fmt.Fprintf(logWriter, "WARNING: Could not find image ID in logs and tag inspection failed: %v\n", inspectErr) + if err == nil { // Si le stream s'est bien terminé mais sans ID + err = fmt.Errorf("build stream finished but image ID could not be determined") + } + } + } else if err == nil { + err = fmt.Errorf("build stream finished but image ID could not be determined (no tags specified)") + } + } + + if err != nil { + return "", err // Retourner l'erreur de stream ou l'erreur "ID non trouvé" + } + + fmt.Fprintf(logWriter, "Docker build finished. Image ID: %s\n", imageID) + return imageID, nil +} \ No newline at end of file diff --git a/bx/build/templates.go b/bx/build/templates.go new file mode 100644 index 0000000..10a0214 --- /dev/null +++ b/bx/build/templates.go @@ -0,0 +1,401 @@ +package build + + +// dockerfileTemplates mappe un identifiant d'écosystème à son template Dockerfile. +// La clé est généralement "Language-PackageManager" ou "Language-Ecosystem". +var DockerfileTemplates = map[string]string{ + // --- Go --- + "Go-go": ` +# --- Build Stage --- +# Utiliser une image Go spécifique (ajuster la version au besoin) +# ARG GOLANG_VERSION=1.21 +# FROM golang:${GOLANG_VERSION}-alpine AS builder +FROM golang:1.21-alpine AS builder + +# Définir le répertoire de travail +WORKDIR /app + +# Installer les outils nécessaires (optionnel, ex: pour CGO) +# RUN apk add --no-cache gcc libc-dev + +# Télécharger les dépendances séparément pour profiter du cache Docker +# Copier go.mod et go.sum (et go.work/go.work.sum si pertinent) +COPY go.* ./ +# RUN go work sync # Décommenter si go.work est utilisé +RUN go mod download + +# Copier le reste du code source +COPY . . + +# Compiler l'application +# Utiliser -ldflags="-w -s" pour réduire la taille du binaire final (optionnel) +# Utiliser CGO_ENABLED=0 pour une compilation statique si possible (pas de dépendances C) +RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /app/main . + +# --- Final Stage --- +# Utiliser une image minimale (alpine est petite, distroless est encore plus minimal) +# FROM gcr.io/distroless/static-debian11 AS final # Pour binaire statique (CGO_ENABLED=0) +FROM alpine:latest AS final + +# Créer un utilisateur non-root pour la sécurité +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +WORKDIR /app + +# Copier le binaire compilé depuis l'étape de build +COPY --from=builder /app/main . + +# Copier les assets statiques ou fichiers de configuration si nécessaire +# COPY --from=builder /app/templates ./templates +# COPY --from=builder /app/static ./static +# COPY config.yaml . + +# Port exposé par l'application (ajuster si nécessaire) +EXPOSE 8080 + +# Commande pour lancer l'application +CMD ["./main"] + +# Note: N'oubliez pas de créer un fichier .dockerignore efficace ! +# Exclure .git, tmp/, *.log, .vscode/, etc. et potentiellement le binaire 'main' local. +`, + + // --- Node.js (NPM) --- + "JavaScript-npm": ` +# --- Build Stage --- +# Utiliser une image Node spécifique (ajuster la version LTS ou autre) +# ARG NODE_VERSION=18 +# FROM node:${NODE_VERSION}-alpine AS builder +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copier package.json et package-lock.json (ou npm-shrinkwrap.json) +COPY package*.json ./ + +# Installer les dépendances (npm ci est recommandé pour la reproductibilité) +# Utilisation du cache mount de BuildKit pour accélérer les installs répétés +RUN --mount=type=cache,target=/root/.npm \ + npm ci --only=production --ignore-scripts --prefer-offline --no-audit + +# Copier le reste du code source de l'application +COPY . . + +# Optionnel: Exécuter le script de build (ex: pour TypeScript, React, Vue, etc.) +# Assurez-vous que les devDependencies sont installées si nécessaire pour le build +# Si besoin de devDependencies: +# RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts --prefer-offline --no-audit +# RUN npm run build + +# --- Final Stage --- +FROM node:18-alpine AS final + +WORKDIR /app + +# Créer un utilisateur non-root +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# Copier les dépendances installées et le code source depuis le builder +# Important: Assurer que les permissions sont correctes pour l'utilisateur non-root +COPY --from=builder --chown=appuser:appgroup /app /app + +USER appuser + +# Port exposé par l'application +EXPOSE 3000 + +# Commande pour lancer l'application (ajuster selon votre point d'entrée) +CMD ["node", "votre-fichier-main.js"] # ou "server.js", "dist/main.js", etc. + +# Note: Utilisez un .dockerignore ! Excluez node_modules, .git, *.log, dist/, build/ etc. +`, + + // --- Node.js (Yarn) --- + "JavaScript-yarn": ` +# --- Build Stage --- +# ARG NODE_VERSION=18 +# FROM node:${NODE_VERSION}-alpine AS builder +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copier package.json et yarn.lock +COPY package.json yarn.lock ./ + +# Installer les dépendances (yarn install --frozen-lockfile est recommandé) +# Utilisation du cache mount de BuildKit pour Yarn v1 (cache par défaut) ou v2+ (ajuster le target) +# Pour Yarn v1: /usr/local/share/.cache/yarn/v6 +# Pour Yarn v2+ (PnP/node_modules): .yarn/cache ou node_modules/.yarn-cache +# Vérifiez votre configuration Yarn Berry. Ici on suppose Yarn v1 ou v2+ avec node_modules linker. +RUN --mount=type=cache,target=/usr/local/share/.cache/yarn/v6 \ + yarn install --frozen-lockfile --production --ignore-scripts --prefer-offline + +# Copier le reste du code source +COPY . . + +# Optionnel: Exécuter le script de build +# Si besoin de devDependencies: +# RUN --mount=type=cache,target=/usr/local/share/.cache/yarn/v6 yarn install --frozen-lockfile --ignore-scripts --prefer-offline +# RUN yarn build + +# --- Final Stage --- +FROM node:18-alpine AS final +WORKDIR /app +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +COPY --from=builder --chown=appuser:appgroup /app /app +USER appuser +EXPOSE 3000 +CMD ["node", "votre-fichier-main.js"] +# Note: Utilisez un .dockerignore ! (node_modules, .yarn/, .git, *.log, etc.) +`, + + // --- Node.js (PNPM) --- + "JavaScript-pnpm": ` +# --- Build Stage --- +# ARG NODE_VERSION=18 +# FROM node:${NODE_VERSION}-alpine AS builder +FROM node:18-alpine AS builder + +# Installer pnpm globalement dans l'image de build +RUN npm install -g pnpm + +WORKDIR /app + +# Copier les fichiers de dépendances +COPY package.json pnpm-lock.yaml ./ +# Copier .npmrc s'il existe (peut contenir des configurations de registry) +# COPY .npmrc . + +# Installer les dépendances (--frozen-lockfile est implicite avec pnpm-lock.yaml) +# Utilisation du cache mount de BuildKit pour le store pnpm (par défaut ~/.pnpm-store) +RUN --mount=type=cache,target=/root/.pnpm-store \ + pnpm install --prod --prefer-offline --ignore-scripts + +# Copier le reste du code source +COPY . . + +# Optionnel: Exécuter le script de build +# Si besoin de devDependencies: +# RUN --mount=type=cache,target=/root/.pnpm-store pnpm install --prefer-offline --ignore-scripts +# RUN pnpm build + +# --- Final Stage --- +# Il est crucial de copier correctement le store pnpm ou les node_modules +# Stratégie 1: Copier tout le répertoire /app (simple mais peut être gros) +FROM node:18-alpine AS final +WORKDIR /app +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +COPY --from=builder --chown=appuser:appgroup /app /app +USER appuser +EXPOSE 3000 +CMD ["node", "votre-fichier-main.js"] + +# Stratégie 2 (plus complexe, pour optimiser la taille): Utiliser 'pnpm deploy' +# FROM node:18-alpine AS builder +# ... (installations comme avant) ... +# RUN pnpm build # Si nécessaire +# RUN pnpm prune --prod # Optionnel, supprime les devDeps si elles ont été installées +# RUN pnpm deploy /prod_app --prod # Crée un répertoire avec seulement les deps de prod +# +# FROM node:18-alpine AS final +# WORKDIR /app +# RUN addgroup -S appgroup && adduser -S appuser -G appgroup +# COPY --from=builder --chown=appuser:appgroup /prod_app /app # Copier le résultat de deploy +# USER appuser +# EXPOSE 3000 +# CMD ["node", "votre-fichier-main.js"] + +# Note: Utilisez un .dockerignore ! (node_modules, .git, *.log, etc.) +`, + + // --- Rust (Cargo) --- + "Rust-cargo": ` +# --- Build Stage (Planner) --- +# Utiliser l'image Rust officielle (ajuster version/toolchain) +# FROM rust:1.70-slim AS planner +FROM rust:1.70-slim AS planner + +WORKDIR /app + +# Copier uniquement les manifestes Cargo +COPY Cargo.toml Cargo.lock* ./ +# Copier les manifestes des workspaces membres si nécessaire +# COPY members/*/Cargo.toml ./members/*/ + +# Créer un projet factice pour pré-compiler les dépendances +# Cela évite de recompiler les dépendances si seul le code src/ change +RUN mkdir src && echo "fn main() {}" > src/main.rs +# Compiler uniquement les dépendances (sans cache mount pour cette étape simple) +RUN cargo build --release --locked + +# --- Build Stage (Builder) --- +# FROM rust:1.70-slim AS builder +FROM rust:1.70-slim AS builder +WORKDIR /app + +# Copier les dépendances pré-compilées du planner +COPY --from=planner /app/target ./target +COPY --from=planner /usr/local/cargo/registry /usr/local/cargo/registry +COPY Cargo.toml Cargo.lock* ./ +# COPY members/*/Cargo.toml ./members/*/ + +# Copier le code source réel +COPY src ./src +# COPY members/*/src ./members/*/ + +# Compiler le projet final +# Utilisation du cache mount de BuildKit pour le cache de compilation incrémentale +RUN --mount=type=cache,target=/app/target \ + --mount=type=cache,target=/usr/local/cargo/registry \ + cargo build --release --locked + +# --- Final Stage --- +# Utiliser une image minimale. Debian slim est un bon compromis. +# Alpine peut nécessiter musl-tools si vous avez des dépendances C. +FROM debian:bullseye-slim AS final +# FROM alpine:latest AS final # Si compatible musl +# RUN apk add --no-cache musl-tools # Si Alpine et besoin de C + +WORKDIR /app + +# Créer un utilisateur non-root +RUN groupadd -r appgroup && useradd --no-log-init -r -g appgroup appuser +USER appuser + +# Copier le binaire compilé +COPY --from=builder /app/target/release/your_binary_name ./ # Remplacez your_binary_name ! + +# Port exposé (ajuster) +EXPOSE 8000 + +# Commande de lancement +CMD ["./your_binary_name"] + +# Note: .dockerignore est crucial ! (target/, .git, etc.) +`, + + // --- Python (Pip) --- + "Python-Pip": ` +# --- Build Stage --- +# Utiliser une image Python officielle (ajuster version) +# ARG PYTHON_VERSION=3.11 +# FROM python:${PYTHON_VERSION}-slim AS builder +FROM python:3.11-slim AS builder + +WORKDIR /app + +# Installer les dépendances système si nécessaire (ex: pour psycopg2, Pillow) +# RUN apt-get update && apt-get install -y --no-install-recommends \ +# build-essential libpq-dev \ +# && rm -rf /var/lib/apt/lists/* + +# Créer un environnement virtuel +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Mettre à jour pip et installer wheel +RUN pip install --upgrade pip wheel + +# Copier le fichier de dépendances +COPY requirements.txt . + +# Installer les dépendances dans l'environnement virtuel +# Utilisation du cache mount de BuildKit pour le cache pip +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --no-cache-dir -r requirements.txt + +# Copier le reste du code source +COPY . . + +# --- Final Stage --- +# FROM python:${PYTHON_VERSION}-slim AS final +FROM python:3.11-slim AS final + +WORKDIR /app + +# Créer un utilisateur non-root +RUN groupadd -r appgroup && useradd --no-log-init -r -g appgroup appuser + +# Copier l'environnement virtuel créé dans l'étape de build +COPY --from=builder /opt/venv /opt/venv + +# Copier le code de l'application +COPY --chown=appuser:appgroup . /app + +# Définir le PATH pour inclure l'environnement virtuel +ENV PATH="/opt/venv/bin:$PATH" +# Empêcher Python d'écrire des fichiers .pyc +ENV PYTHONDONTWRITEBYTECODE 1 +# Assurer que Python tourne en mode non-bufferisé (bon pour les logs) +ENV PYTHONUNBUFFERED 1 + +USER appuser + +# Port exposé (ajuster) +EXPOSE 8000 + +# Commande de lancement (ajuster selon votre application: gunicorn, uvicorn, python main.py) +# CMD ["gunicorn", "-b", "0.0.0.0:8000", "your_project.wsgi:application"] +CMD ["python", "your_main_script.py"] + +# Note: .dockerignore (venv/, __pycache__/, .git, *.log, *.db, etc.) +`, + + // --- Java (Maven) --- + "Java-Maven": ` +# --- Build Stage --- +# Utiliser une image Maven avec un JDK spécifique (ajuster versions) +# ARG MAVEN_VERSION=3.8 +# ARG JDK_VERSION=17 +# FROM maven:${MAVEN_VERSION}-eclipse-temurin-${JDK_VERSION}-alpine AS builder +FROM maven:3.8-eclipse-temurin-17-alpine AS builder + +WORKDIR /app + +# Copier le fichier pom.xml +COPY pom.xml . + +# Télécharger les dépendances Maven +# Utilisation du cache mount de BuildKit pour le dépôt local Maven (.m2) +RUN --mount=type=cache,target=/root/.m2 \ + mvn dependency:go-offline -B + +# Copier le code source +COPY src ./src + +# Compiler et packager l'application (ex: en JAR ou WAR) +# Le cache mount ici accélère la compilation si les sources n'ont pas changé +RUN --mount=type=cache,target=/root/.m2 \ + mvn package -B -DskipTests + +# --- Final Stage --- +# Utiliser une image JRE minimale (ajuster version et distribution) +# FROM eclipse-temurin:${JDK_VERSION}-jre-alpine AS final +FROM eclipse-temurin:17-jre-alpine AS final + +WORKDIR /app + +# Créer un utilisateur non-root +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +# Copier l'artefact buildé (JAR/WAR) depuis l'étape de build +# Ajuster le chemin du JAR/WAR selon la configuration de votre pom.xml +COPY --from=builder /app/target/*.jar ./app.jar +# COPY --from=builder /app/target/*.war ./app.war + +# Port exposé (ajuster) +EXPOSE 8080 + +# Commande de lancement (ajuster) +# Pour un JAR exécutable: +CMD ["java", "-jar", "app.jar"] +# Pour un WAR (nécessite un serveur d'application comme Tomcat, non inclus ici) +# CMD ["catalina.sh", "run"] # Si l'image de base était Tomcat + +# Note: .dockerignore (target/, .git, .mvn/, *.log, etc.) +`, + + // Ajouter d'autres templates ici (Gradle, PHP/Composer, Ruby/Bundler, etc.) +} \ No newline at end of file diff --git a/bx/cmd/run.go b/bx/cmd/run.go new file mode 100644 index 0000000..00ecf6e --- /dev/null +++ b/bx/cmd/run.go @@ -0,0 +1,190 @@ +// cmd/bx/cmd/run.go +package cmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" // Pour docker load + + "anexis/bx/build" + "anexis/socket" // Pour parser RunYAML + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var ( + runFile string + // servicesToRun []string // Pour exécuter seulement certains services + // detach bool // Pour exécuter en arrière-plan + + runCmd = &cobra.Command{ + Use: "run -f ", + Short: "Lance les services définis dans un fichier .run.yml généré par un build.", + Long: `Cette commande lit un fichier .run.yml, interprète les définitions de service +et lance les conteneurs correspondants en utilisant la commande 'docker run'. +Elle gère le chargement des images locales si nécessaire.`, + Args: cobra.NoArgs, + RunE: runRunCommand, + } +) + +func init() { + runCmd.Flags().StringVarP(&runFile, "file", "f", "", "Chemin vers le fichier .run.yml (obligatoire)") + // runCmd.Flags().StringSliceVarP(&servicesToRun, "service", "", []string{}, "Spécifier les services à lancer (défaut: tous)") + // runCmd.Flags().BoolVarP(&detach, "detach", "d", false, "Lancer les conteneurs en arrière-plan (détaché)") + runCmd.MarkFlagRequired("file") +} + +func runRunCommand(cmd *cobra.Command, args []string) error { + if runFile == "" { + return fmt.Errorf("le flag --file (-f) est obligatoire") + } + if _, err := os.Stat(runFile); os.IsNotExist(err) { + return fmt.Errorf("le fichier .run.yml '%s' n'existe pas", runFile) + } + + // 1. Lire et parser le fichier .run.yml + runData, err := os.ReadFile(runFile) + if err != nil { + return fmt.Errorf("erreur lors de la lecture de '%s': %w", runFile, err) + } + + var runConfig build.Bui + err = yaml.Unmarshal(runData, &runConfig) + if err != nil { + return fmt.Errorf("erreur lors du parsing YAML de '%s': %w", runFile, err) + } + + if len(runConfig.Services) == 0 { + fmt.Println("Aucun service défini dans", runFile) + return nil + } + + fmt.Printf("Lancement des services depuis '%s'...\n", runFile) + runFileDir := filepath.Dir(runFile) // Répertoire où se trouve le run.yml (pour les paths relatifs des .tar) + + // 2. Itérer et lancer chaque service + // TODO: Gérer l'ordre basé sur depends_on si nécessaire (complexe avec docker run) + for serviceName, service := range runConfig.Services { + fmt.Printf("--- Lancement du service: %s ---\n", serviceName) + + // Construire la commande docker run + dockerArgs := []string{"run"} + + // Détaché ? + // if detach { dockerArgs = append(dockerArgs, "-d") } else { + // Pour la simplicité, on ajoute --rm pour nettoyer après arrêt foreground + dockerArgs = append(dockerArgs, "--rm") + // } + // Ajouter -it pour interactivité si pas détaché ? Peut causer problèmes. + // dockerArgs = append(dockerArgs, "-it") + + // Nom du conteneur (basé sur service) + containerName := fmt.Sprintf("bx_run_%s_%d", serviceName, time.Now().UnixNano()) + dockerArgs = append(dockerArgs, "--name", containerName) + + // Politique de redémarrage + if service.Restart != "" { + dockerArgs = append(dockerArgs, "--restart", service.Restart) + } + + // Variables d'environnement + for key, val := range service.Environment { + dockerArgs = append(dockerArgs, "-e", fmt.Sprintf("%s=%s", key, val)) + } + + // Ports + for _, portMapping := range service.Ports { + dockerArgs = append(dockerArgs, "-p", portMapping) + } + + // Volumes + for _, volumeMapping := range service.Volumes { + // Attention: Interpréter les chemins relatifs pour les bind mounts + parts := strings.SplitN(volumeMapping, ":", 2) + if len(parts) == 2 && !filepath.IsAbs(parts[0]) && !strings.Contains(parts[0], "/") { + // Probablement un volume nommé, laisser tel quel + dockerArgs = append(dockerArgs, "-v", volumeMapping) + } else if len(parts) >= 2 && !filepath.IsAbs(parts[0]) { + // Chemin hôte relatif -> le rendre absolu par rapport à ?? CWD? run.yml dir? + // Soyons prudents, n'autorisons que les chemins absolus ou volumes nommés pour l'instant + fmt.Printf("WARN: Le chemin hôte relatif '%s' dans le volume mapping n'est pas supporté. Utilisez un chemin absolu ou un volume nommé.\n", parts[0]) + // dockerArgs = append(dockerArgs, "-v", volumeMapping) // Ou skipper ? + } else { + dockerArgs = append(dockerArgs, "-v", volumeMapping) // Volume nommé ou chemin absolu + } + } + + // Image + imageRef := service.Image + if strings.HasSuffix(imageRef, ".tar") { + // Assumer que c'est un fichier .tar local relatif au .run.yml + tarPath := imageRef + if !filepath.IsAbs(tarPath) { + tarPath = filepath.Join(runFileDir, tarPath) + } + fmt.Printf("Chargement de l'image depuis l'archive locale: %s\n", tarPath) + if _, err := os.Stat(tarPath); os.IsNotExist(err) { + return fmt.Errorf("l'archive image '%s' pour le service '%s' n'existe pas", tarPath, serviceName) + } + + loadCmd := exec.Command("docker", "load", "-i", tarPath) + loadCmd.Stdout = os.Stdout + loadCmd.Stderr = os.Stderr + if err := loadCmd.Run(); err != nil { + return fmt.Errorf("erreur lors du chargement de l'image depuis '%s': %w", tarPath, err) + } + // Comment obtenir le tag/ID chargé ? docker load l'affiche. C'est compliqué. + // On suppose que le tar contient une image tagguée de manière prévisible. + // => Il FAUT que le build.go (lorsqu'il sauve en local) taggue l'image avant de la sauver. + // => Le run.yml doit référencer ce TAG, pas le .tar. + // ---> REVISION NECESSAIRE de la génération du run.yml pour storage "local" ! + // Pour l'instant, on va supposer que le .tar contient l'image service.Image (sans le .tar) + // Ceci est une GROSSE supposition. + imageRef = strings.TrimSuffix(service.Image, ".tar") // Suppose que le tag est le nom du fichier sans .tar + fmt.Printf("Supposition : l'image chargée devrait être tagguée comme '%s'\n", imageRef) + + } else if strings.HasPrefix(imageRef, "local:") { + // Gérer l'autre cas de fallback de getImageRefForRun + return fmt.Errorf("référence d'image locale non trouvée '%s' pour le service '%s'", imageRef, serviceName) + } + dockerArgs = append(dockerArgs, imageRef) // Ajouter l'image (tag ou ID) + + // Entrypoint / Command + if len(service.Entrypoint) > 0 { + dockerArgs = append(dockerArgs, "--entrypoint", service.Entrypoint[0]) // docker run prend seulement le premier + // Ajouter les arguments d'entrypoint après l'image + //dockerArgs = append(dockerArgs, service.Entrypoint[1:]...) // Non, ça c'est la commande + } + if len(service.Command) > 0 { + // La commande vient après l'image (et après les args d'entrypoint s'il y en a) + dockerArgs = append(dockerArgs, service.Command...) + } + + // Exécuter la commande docker run + fmt.Printf("Exécution: docker %s\n", strings.Join(dockerArgs, " ")) + runCmd := exec.CommandContext(context.Background(), "docker", dockerArgs...) // Utiliser un contexte ? + runCmd.Stdout = os.Stdout + runCmd.Stderr = os.Stderr + // runCmd.Stdin = os.Stdin // Pour interactivité ? + + err = runCmd.Run() // Bloque jusqu'à la fin du conteneur (car pas -d) + if err != nil { + // Si le conteneur s'arrête avec un code non-nul, Run() retourne une erreur + fmt.Printf("Erreur lors de l'exécution du service '%s': %v\n", serviceName, err) + // Faut-il arrêter les autres services ? Pour l'instant, on continue. + // return fmt.Errorf("le service '%s' a échoué: %w", serviceName, err) // Arrêter tout + } else { + fmt.Printf("--- Service '%s' terminé ---\n", serviceName) + } + fmt.Println() // Ligne vide entre les services + } + + fmt.Println("Tous les services ont été lancés.") + return nil +} \ No newline at end of file diff --git a/bx/go.mod b/bx/go.mod index 85c7689..7e235d3 100644 --- a/bx/go.mod +++ b/bx/go.mod @@ -4,12 +4,44 @@ go 1.24.2 require ( github.com/docker/docker v28.1.1+incompatible + github.com/go-git/go-git/v5 v5.16.0 + github.com/joho/godotenv v1.5.1 + github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 ) +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + google.golang.org/grpc v1.71.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + require ( github.com/Backblaze/blazer v0.7.2 - github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -20,13 +52,13 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/go-archive v0.1.0 github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect + github.com/moby/term v0.5.2 github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -38,6 +70,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/time v0.11.0 // indirect ) diff --git a/bx/go.sum b/bx/go.sum index 0a6ef2b..60f39a6 100644 --- a/bx/go.sum +++ b/bx/go.sum @@ -1,18 +1,35 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM= github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= @@ -21,8 +38,22 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -30,19 +61,31 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -63,26 +106,41 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -108,29 +166,43 @@ go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -141,17 +213,22 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/components/run.go b/cli/components/run.go new file mode 100644 index 0000000..dd29554 --- /dev/null +++ b/cli/components/run.go @@ -0,0 +1,214 @@ +package components + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +type Component struct { + Name string `json:"name"` + InstalledAt string `json:"installed_at"` + Options map[string]string `json:"options,omitempty"` +} + +type LockFile struct { + Components []Component `json:"components"` +} + +const lockFilePath = "../../frontend/components.lock.json" +const componentsDir = "../../frontend/components" + +// Couleurs terminal +const ( + green = "\033[32m" + red = "\033[31m" + blue = "\033[34m" + reset = "\033[0m" +) + +// func main() { +// if len(os.Args) < 2 { +// fmt.Println(blue + "Usage:" + reset + " go run install_components.go [install|add|remove] [component-name(s)]") +// return +// } + +// command := os.Args[1] + +// switch command { +// case "install": +// installComponents() +// case "add": +// addComponents(os.Args[2:]) +// case "remove": +// removeComponents(os.Args[2:]) +// default: +// fmt.Println(red + "Commande inconnue:" + reset, command) +// } +// } + +func loadLockFile() (*LockFile, error) { + var lock LockFile + file, err := os.ReadFile(lockFilePath) + if err != nil { + return &LockFile{}, nil + } + err = json.Unmarshal(file, &lock) + if err != nil { + return nil, err + } + return &lock, nil +} + +func saveLockFile(lock *LockFile) error { + data, err := json.MarshalIndent(lock, "", " ") + if err != nil { + return err + } + return os.WriteFile(lockFilePath, data, 0644) +} + +func isComponentInstalled(name string) bool { + componentPath := filepath.Join(componentsDir, name) + _, err := os.Stat(componentPath) + return !os.IsNotExist(err) +} + +func InstallComponents() { + lock, err := loadLockFile() + if err != nil { + fmt.Println(red+"Error during the lockfile reading:"+reset, err) + return + } + + for _, comp := range lock.Components { + if !isComponentInstalled(comp.Name) { + fmt.Println(blue+"Installation of"+reset, comp.Name, "...") + err := runShadcnAdd(comp.Name) + if err != nil { + fmt.Println(red+"Error during the installation of"+reset, comp.Name, ":", err) + } + } else { + fmt.Println(green + comp.Name + " already installed ✅" + reset) + } + } + + fmt.Println(green + "Installation finished." + reset) +} + +func AddComponents(names []string) { + if len(names) == 0 { + fmt.Println(red + "No component to add." + reset) + return + } + + lock, err := loadLockFile() + if err != nil { + fmt.Println(red+"Error for the lockfile reading:"+reset, err) + return + } + + for _, name := range names { + name = strings.TrimSpace(name) + if name == "" { + continue + } + + // Vérifier si déjà locké + if componentExists(lock, name) { + fmt.Println(green + name + " already in the lockfile ✅" + reset) + continue + } + + fmt.Println(blue+"Ajout de"+reset, name, "...") + + err := runShadcnAdd(name) + if err != nil { + fmt.Println(red+"Erreur ajout de"+reset, name, ":", err) + continue + } + + newComponent := Component{ + Name: name, + InstalledAt: time.Now().UTC().Format(time.RFC3339), + Options: map[string]string{}, + } + lock.Components = append(lock.Components, newComponent) + + fmt.Println(green + name + " ajouté et installé 🔥" + reset) + } + + err = saveLockFile(lock) + if err != nil { + fmt.Println(red+"Erreur sauvegarde lock file:"+reset, err) + } +} + +func RemoveComponents(names []string) { + if len(names) == 0 { + fmt.Println(red + "Aucun composant à supprimer." + reset) + return + } + + lock, err := loadLockFile() + if err != nil { + fmt.Println(red+"Erreur lecture lock file:"+reset, err) + return + } + + for _, name := range names { + name = strings.TrimSpace(name) + if name == "" { + continue + } + + // Supprimer le dossier localement + componentPath := filepath.Join(componentsDir, name) + err := os.RemoveAll(componentPath) + if err != nil { + fmt.Println(red+"Erreur suppression composant:"+reset, name, ":", err) + continue + } + + // Supprimer du lock file + lock.Components = removeComponentFromLock(lock, name) + + fmt.Println(green + name + " supprimé ✅" + reset) + } + + err = saveLockFile(lock) + if err != nil { + fmt.Println(red+"Erreur sauvegarde lock file:"+reset, err) + } +} + +func runShadcnAdd(name string) error { + cmd := exec.Command("npx", "shadcn-ui@latest", "add", name) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func componentExists(lock *LockFile, name string) bool { + for _, c := range lock.Components { + if c.Name == name { + return true + } + } + return false +} + +func removeComponentFromLock(lock *LockFile, name string) []Component { + newComponents := []Component{} + for _, c := range lock.Components { + if c.Name != name { + newComponents = append(newComponents, c) + } + } + return newComponents +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..6dc8ac5 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,3 @@ +module anexis/cli + +go 1.24.2 diff --git a/socket/client.go b/socket/client.go new file mode 100644 index 0000000..0ee58de --- /dev/null +++ b/socket/client.go @@ -0,0 +1,262 @@ +// socket/client.go +package socket + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/google/uuid" // Pour générer les RequestID + "github.com/gorilla/websocket" +) + +// Client gère la connexion WebSocket sortante. +type Client struct { + conn *connection // Wrapper de connexion partagé + + Incoming chan *Message // Canal public pour recevoir les messages du serveur + Done chan struct{} // Canal pour signaler l'arrêt du client + + mu sync.Mutex + isConnected bool + dialer *websocket.Dialer + connUrl string + headers http.Header // Pour l'authentification ou autres en-têtes + + // Pour corréler requêtes/réponses + pendingRequests map[string]chan *Message + pendingMu sync.RWMutex +} + +// NewClient crée une nouvelle instance client. +func NewClient() *Client { + return &Client{ + Incoming: make(chan *Message, 100), // Buffer pour les messages entrants + Done: make(chan struct{}), + dialer: websocket.DefaultDialer, + pendingRequests: make(map[string]chan *Message), + } +} + +// Connect établit la connexion WebSocket avec le serveur. +func (c *Client) Connect(serverUrl string, headers http.Header) error { + c.mu.Lock() + if c.isConnected { + c.mu.Unlock() + return fmt.Errorf("client already connected") + } + c.connUrl = serverUrl + c.headers = headers + c.mu.Unlock() + + log.Printf("Client: Attempting to connect to %s...\n", serverUrl) + ws, resp, err := c.dialer.Dial(c.connUrl, c.headers) + if err != nil { + errMsg := fmt.Sprintf("Client: Failed to connect to %s: %v", c.connUrl, err) + if resp != nil { + errMsg = fmt.Sprintf("%s (Status: %s)", errMsg, resp.Status) + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if len(body) > 0 { + errMsg = fmt.Sprintf("%s - Body: %s", errMsg, string(body)) + } + } + return fmt.Errorf("an error occurred %s", errMsg) + } + log.Printf("Client: Successfully connected to %s\n", c.connUrl) + + c.mu.Lock() + c.conn = newConnection(ws) + c.isConnected = true + // Réinitialiser Done channel au cas où il aurait été fermé par une connexion précédente + // c.Done = make(chan struct{}) // Non, Done signale la fin définitive du client + c.mu.Unlock() + + // Démarrer les pompes + go c.conn.writePump() + go c.conn.readPump(c.handleIncomingMessage, c.handleDisconnect) + + return nil +} + +// IsConnected retourne l'état de la connexion. +func (c *Client) IsConnected() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.isConnected +} + +// handleIncomingMessage traite les messages reçus du serveur. +func (c *Client) handleIncomingMessage(msg *Message, conn *connection) error { + // log.Printf("Client: Received message type %s (ReqID: %s)\n", msg.Type, msg.RequestID) // Debug + + // Vérifier si c'est une réponse à une requête en attente + c.pendingMu.Lock() // Prendre le verrou en écriture car on va delete + if msg.RequestID != "" { + if respChan, ok := c.pendingRequests[msg.RequestID]; ok { + // C'est une réponse directe à une requête envoyée via SendRequest + log.Printf("Client: Correlated response for RequestID %s\n", msg.RequestID) + select { + case respChan <- msg: // Envoyer la réponse au demandeur + default: + // Le demandeur n'écoute plus (timeout?), logguer mais continuer + log.Printf("Warning: No listener for response channel of RequestID %s\n", msg.RequestID) + } + // Supprimer la requête en attente après avoir envoyé la réponse + delete(c.pendingRequests, msg.RequestID) + c.pendingMu.Unlock() // Libérer le verrou ici + return nil // Le message a été traité comme une réponse directe + } + } + c.pendingMu.Unlock() // Libérer le verrou si ce n'était pas une réponse corrélée + + // Si ce n'est pas une réponse directe, le pousser sur le canal public Incoming + select { + case c.Incoming <- msg: + // Message transmis à l'application utilisatrice + default: + // Le canal Incoming est plein, l'application ne lit pas assez vite + log.Printf("Warning: Client Incoming channel full. Message type %s dropped.\n", msg.Type) + } + return nil // Succès du traitement (mis en file d'attente) +} + +// handleDisconnect est appelé lorsque la connexion est perdue. +func (c *Client) handleDisconnect(conn *connection) { + c.mu.Lock() + // Vérifier si c'est bien la connexion actuelle qui est déconnectée + if c.conn != conn { + c.mu.Unlock() + log.Printf("Client: Received disconnect signal for an old/stale connection (%p)\n", conn.ws) + return // Ne rien faire si ce n'est pas la connexion active + } + c.isConnected = false + c.conn = nil // Libérer la référence à la connexion + log.Println("Client: Connection lost.") + // Ne pas fermer c.Done ici, l'utilisateur peut vouloir reconnecter. + // Fermer Done seulement quand Close() est appelé explicitement. + c.mu.Unlock() + + // Nettoyer les requêtes en attente pour cette connexion perdue + c.pendingMu.Lock() + if len(c.pendingRequests) > 0 { + log.Printf("Client: Cleaning up %d pending requests due to disconnect.\n", len(c.pendingRequests)) + for reqID, respChan := range c.pendingRequests { + // Envoyer une erreur ou fermer le canal pour débloquer les appelants + // Fermer le canal est plus sûr pour signaler la fin. + close(respChan) + delete(c.pendingRequests, reqID) + } + } + c.pendingMu.Unlock() +} + +// Send envoie un message au serveur de manière asynchrone. +// Ne garantit pas la réception et ne gère pas les réponses. +func (c *Client) Send(msg *Message) error { + c.mu.Lock() + conn := c.conn + isConnected := c.isConnected + c.mu.Unlock() + + if !isConnected || conn == nil { + return fmt.Errorf("client not connected") + } + // log.Printf("Client: Sending message type %s async\n", msg.Type) // Debug + conn.sendMsg(msg) + return nil +} + +// SendRequest envoie un message et attend une réponse corrélée par RequestID. +func (c *Client) SendRequest(ctx context.Context, msgType EventType, payload interface{}) (*Message, error) { + c.mu.Lock() + conn := c.conn + isConnected := c.isConnected + c.mu.Unlock() + + if !isConnected || conn == nil { + return nil, fmt.Errorf("client not connected") + } + + // Générer un RequestID unique + requestID := uuid.NewString() + msg := NewMessage(msgType, requestID) + if payload != nil { + if err := msg.AddPayload(payload); err != nil { + return nil, fmt.Errorf("failed to add payload for request %s: %w", requestID, err) + } + } + + // Créer un canal pour recevoir la réponse + respChan := make(chan *Message, 1) // Buffer 1 au cas où la réponse arrive avant qu'on écoute + + c.pendingMu.Lock() + c.pendingRequests[requestID] = respChan + c.pendingMu.Unlock() + + // Nettoyer la requête en attente à la fin (succès, erreur, timeout) + defer func() { + c.pendingMu.Lock() + delete(c.pendingRequests, requestID) + c.pendingMu.Unlock() + // Ne pas fermer respChan ici, car on peut le lire après la sortie + }() + + // Envoyer la requête + log.Printf("Client: Sending request %s (Type: %s)\n", requestID, msg.Type) + conn.sendMsg(msg) // Envoi asynchrone + + // Attendre la réponse ou l'annulation/timeout du contexte + select { + case resp := <-respChan: + // Réponse reçue ! + log.Printf("Client: Received response for request %s (Type: %s, Error: '%s')\n", requestID, resp.Type, resp.Error) + if resp.Error != "" || resp.Type == EvtError { + // Le serveur a renvoyé une erreur pour cette requête + errMsg := resp.Error + if errMsg == "" { errMsg = "received error event" } // Cas où Error est vide mais Type est EvtError + var errPayload ErrorPayload + if resp.DecodePayload(&errPayload) == nil && errPayload.Details != "" { + errMsg = fmt.Sprintf("%s: %s", errMsg, errPayload.Details) + } + return nil, fmt.Errorf("server error response for request %s: %s", requestID, errMsg) + } + // Réponse réussie + return resp, nil + + case <-ctx.Done(): + // Le contexte a été annulé (timeout ou autre) + log.Printf("Client: Context done while waiting for response to request %s: %v\n", requestID, ctx.Err()) + return nil, fmt.Errorf("request %s timed out or was canceled: %w", requestID, ctx.Err()) + + // Gérer le cas où la connexion est perdue pendant l'attente. + // La lecture de respChan échouera si handleDisconnect ferme le canal. + // Il faut vérifier la valeur reçue. + // case resp, ok := <-respChan: + // if !ok { + // return nil, fmt.Errorf("connection closed while waiting for response to request %s", requestID) + // } + // // Traiter resp comme ci-dessus + } +} + + +// Close ferme la connexion WebSocket et arrête le client. +func (c *Client) Close() { + c.mu.Lock() + defer c.mu.Unlock() + + log.Println("Client: Close called.") + // Signaler l'arrêt aux utilisateurs externes + close(c.Done) + + if c.conn != nil && c.isConnected { + // Fermer le canal d'envoi arrêtera la writePump, qui fermera le ws.Conn + c.conn.closeSend() + // La readPump s'arrêtera aussi à cause de la fermeture par la writePump ou un read error. + } + c.isConnected = false // Marquer comme déconnecté +} \ No newline at end of file diff --git a/socket/conn.go b/socket/conn.go new file mode 100644 index 0000000..2a45530 --- /dev/null +++ b/socket/conn.go @@ -0,0 +1,178 @@ +// socket/conn.go +package socket + +import ( + "encoding/json" + "log" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // Temps max pour écrire un message. + writeWait = 10 * time.Second + // Temps max pour lire le prochain message pong du peer. + pongWait = 60 * time.Second + // Envoyer des pings au peer avec cette période. Doit être inférieur à pongWait. + pingPeriod = (pongWait * 9) / 10 + // Taille maximale des messages lus. + maxMessageSize = 8192 // Ajuster si de gros messages sont attendus (ex: build spec) +) + +// connection est un wrapper autour de websocket.Conn gérant les pompes lecture/écriture. +type connection struct { + ws *websocket.Conn + send chan *Message // Channel pour envoyer des messages sortants +} + +// newConnection crée une nouvelle structure de connexion. +func newConnection(ws *websocket.Conn) *connection { + return &connection{ + ws: ws, + send: make(chan *Message, 256), // Buffer pour éviter blocage sur écritures rapides + } +} + +// write pompe les messages du channel 'send' vers la connexion WebSocket. +func (c *connection) write(msgType int, payload []byte) error { + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + return c.ws.WriteMessage(msgType, payload) +} + +// writePump gère l'écriture des messages et les pings périodiques. +// Doit être lancée dans une goroutine séparée par connexion. +func (c *connection) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.ws.Close() // Ferme la connexion WebSocket si la pompe d'écriture s'arrête + log.Println("writePump: Stopped and closed WebSocket connection") + }() + + for { + select { + case message, ok := <-c.send: + if !ok { + // Le channel 'send' a été fermé. Fermer la connexion. + log.Println("writePump: Send channel closed, closing connection.") + c.write(websocket.CloseMessage, []byte{}) + return + } + + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + w, err := c.ws.NextWriter(websocket.TextMessage) // Utiliser TextMessage pour JSON + if err != nil { + log.Printf("writePump: Error getting next writer: %v\n", err) + return // Erreur critique, arrêter la pompe + } + jsonBytes, err := json.Marshal(message) + if err != nil { + log.Printf("writePump: Error marshaling message type %s: %v\n", message.Type, err) + // Ne pas retourner, essayer d'envoyer le prochain message + w.Close() // Fermer le writer actuel + continue + } + + _, err = w.Write(jsonBytes) + if err != nil { + log.Printf("writePump: Error writing JSON: %v\n", err) + // Ne pas retourner ici, mais fermer le writer est important + } + + // Fermer le writer pour flusher le message sur le réseau. + if err := w.Close(); err != nil { + log.Printf("writePump: Error closing writer: %v\n", err) + return // Erreur critique, arrêter la pompe + } + // log.Printf("writePump: Sent message type %s", message.Type) // Debug + + case <-ticker.C: + // Envoyer un ping périodique + // log.Println("writePump: Sending ping") // Debug + if err := c.write(websocket.PingMessage, nil); err != nil { + log.Printf("writePump: Error sending ping: %v\n", err) + return // Erreur critique, arrêter la pompe + } + } + } +} + +// readPump lit les messages entrants et les passe au handler fourni. +// Le handler est responsable du traitement métier. +// Doit être lancée dans une goroutine séparée par connexion. +func (c *connection) readPump(handler func(msg *Message, conn *connection) error, disconnect func(conn *connection)) { + defer func() { + disconnect(c) // S'assurer que la déconnexion est gérée + c.ws.Close() // Ferme la connexion WebSocket si la pompe de lecture s'arrête + log.Println("readPump: Stopped and closed WebSocket connection") + }() + + c.ws.SetReadLimit(maxMessageSize) + c.ws.SetReadDeadline(time.Now().Add(pongWait)) // Attendre le premier message ou pong + c.ws.SetPongHandler(func(string) error { + // log.Println("readPump: Received pong") // Debug + c.ws.SetReadDeadline(time.Now().Add(pongWait)) // Repousser l'échéance à la réception d'un pong + return nil + }) + + for { + msgType, messageBytes, err := c.ws.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + log.Printf("readPump: WebSocket read error: %v\n", err) + } else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + log.Printf("readPump: WebSocket closed normally: %v\n", err) + } else { + log.Printf("readPump: Unhandled WebSocket read error: %v\n", err) + } + break // Sortir de la boucle en cas d'erreur de lecture ou de fermeture + } + + // Ignorer les messages non-texte pour le moment + if msgType != websocket.TextMessage { + log.Printf("readPump: Received non-text message type: %d\n", msgType) + continue + } + + // log.Printf("readPump: Received raw message: %s", string(messageBytes)) // Debug + + var msg Message + if err := json.Unmarshal(messageBytes, &msg); err != nil { + log.Printf("readPump: Error unmarshaling message: %v --- Raw: %s\n", err, string(messageBytes)) + // Envoyer un message d'erreur au client ? Ou juste ignorer ? Ignorons pour l'instant. + // errMsg := NewErrorMessage("", "Invalid message format", err.Error()) + // c.send <- errMsg + continue + } + + // Appeler le handler métier avec le message décodé + if err := handler(&msg, c); err != nil { + log.Printf("readPump: Error handling message type %s: %v\n", msg.Type, err) + // Envoyer une réponse d'erreur liée à la requête si possible + errMsg := NewErrorMessage(msg.RequestID, "Failed to handle request", err.Error()) + c.send <- errMsg // Mettre dans le canal d'envoi + // Faut-il fermer la connexion ici ? Probablement pas, laisser le client décider. + } + + // Réinitialiser le délai de lecture après avoir traité un message (pas strictement nécessaire si on reçoit des pongs) + // c.ws.SetReadDeadline(time.Now().Add(pongWait)) + } +} + +// sendMsg envoie un message de manière asynchrone via le channel. +func (c *connection) sendMsg(msg *Message) { + select { + case c.send <- msg: + // Message envoyé avec succès (mis en file d'attente) + default: + // Le buffer d'envoi est plein, le client est peut-être trop lent ou déconnecté + log.Printf("Warning: Send channel full for connection %p. Message type %s dropped.\n", c.ws, msg.Type) + // Potentiellement fermer la connexion ici si c'est un problème récurrent. + } +} + +// close ferme le canal d'envoi, ce qui arrêtera la writePump. +func (c *connection) closeSend() { + close(c.send) +} \ No newline at end of file diff --git a/socket/go.mod b/socket/go.mod new file mode 100644 index 0000000..f88304d --- /dev/null +++ b/socket/go.mod @@ -0,0 +1,15 @@ +module anexis/socket + +go 1.24.2 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/socket/go.sum b/socket/go.sum new file mode 100644 index 0000000..2ef54c4 --- /dev/null +++ b/socket/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/socket/hub.go b/socket/hub.go new file mode 100644 index 0000000..422c71e --- /dev/null +++ b/socket/hub.go @@ -0,0 +1,89 @@ +// socket/hub.go +package socket + +import ( + "log" + "sync" +) + +// Hub maintient l'ensemble des clients actifs et diffuse les messages. +type Hub struct { + clients map[*connection]bool // Utiliser la connexion comme clé, booléen juste pour la présence + register chan *connection // Canal pour enregistrer de nouveaux clients + unregister chan *connection // Canal pour désenregistrer les clients + // broadcast chan *Message // Si vous avez besoin de diffuser à tous (moins utile pour ce cas d'usage) + + // Mutex pour protéger l'accès à la map clients + mu sync.RWMutex + + // Référence au handler métier pour traiter les messages entrants + messageHandler func(msg *Message, client *connection) error +} + +// newHub crée un nouveau Hub. +func newHub(handler func(msg *Message, client *connection) error) *Hub { + return &Hub{ + clients: make(map[*connection]bool), + register: make(chan *connection), + unregister: make(chan *connection), + // broadcast: make(chan *Message), + messageHandler: handler, + } +} + +// run démarre la boucle principale du Hub pour gérer les enregistrements/désenregistrements. +// Doit être lancée dans une goroutine. +func (h *Hub) run() { + log.Println("Hub: Starting run loop") + for { + select { + case conn := <-h.register: + h.mu.Lock() + h.clients[conn] = true + h.mu.Unlock() + log.Printf("Hub: Client registered (%p). Total clients: %d\n", conn.ws, len(h.clients)) + + case conn := <-h.unregister: + h.mu.Lock() + if _, ok := h.clients[conn]; ok { + delete(h.clients, conn) + // Important: Fermer le canal d'envoi de la connexion pour arrêter sa writePump + conn.closeSend() + log.Printf("Hub: Client unregistered (%p). Total clients: %d\n", conn.ws, len(h.clients)) + } else { + log.Printf("Hub: Unregister request for non-existent client (%p)\n", conn.ws) + } + h.mu.Unlock() + + /* // Logique de broadcast si nécessaire + case message := <-h.broadcast: + h.mu.RLock() + for conn := range h.clients { + select { + case conn.send <- message: + default: + log.Printf("Hub: Broadcast failed for client %p, closing its send channel.\n", conn.ws) + close(conn.send) // Ferme le canal pour ce client lent/mort + delete(h.clients, conn) // Supprimer immédiatement ? Attention à l'itération RLock + } + } + h.mu.RUnlock() + */ + } + } +} + +// handleDisconnect est appelé par la readPump d'une connexion lorsqu'elle se termine. +func (h *Hub) handleDisconnect(conn *connection) { + h.unregister <- conn +} + +// handleIncomingMessage est passé à la readPump pour traiter les messages reçus. +func (h *Hub) handleIncomingMessage(msg *Message, conn *connection) error { + // Logique métier déléguée au handler fourni lors de la création du hub + if h.messageHandler != nil { + return h.messageHandler(msg, conn) + } + log.Printf("Hub: No message handler configured, dropping message type %s from %p\n", msg.Type, conn.ws) + return nil // Ne pas retourner d'erreur pour ne pas fermer la connexion +} \ No newline at end of file diff --git a/socket/message.go b/socket/message.go new file mode 100644 index 0000000..8c336b1 --- /dev/null +++ b/socket/message.go @@ -0,0 +1,127 @@ +// socket/message.go +package socket + +import ( + "encoding/json" + "fmt" +) + +// EventType définit le type de message échangé. +type EventType string + +// Constantes pour les types d'événements possibles. +const ( + // Client -> Server + EvtBuildRequest EventType = "build_request" // Demande de lancement d'un build + EvtSecretRequest EventType = "secret_request" // Demande de récupération d'un secret + + // Server -> Client + EvtBuildQueued EventType = "build_queued" // Accusé de réception, build mis en file d'attente + EvtLogChunk EventType = "log_chunk" // Un morceau de log du build + EvtBuildStatus EventType = "build_status" // Mise à jour du statut (running, success, failure) + EvtSecretResponse EventType = "secret_response" // Réponse à une demande de secret + EvtError EventType = "error" // Message d'erreur général ou spécifique à une requête + + // Bidirectionnel (peut-être moins utile ici, mais possible) + // EvtPing EventType = "ping" + // EvtPong EventType = "pong" +) + +// Message est la structure de base échangée via WebSocket. +type Message struct { + Type EventType `json:"type"` // Type d'événement (obligatoire) + RequestID string `json:"request_id,omitempty"` // ID unique pour corréler req/resp (généré par le client) + Payload json.RawMessage `json:"payload,omitempty"` // Données spécifiques à l'événement (JSON brut) + Error string `json:"error,omitempty"` // Message d'erreur si Type=EvtError ou pour une réponse négative +} + +// --- Payloads Spécifiques --- +// (Définir des structs pour les payloads complexes améliore la lisibilité et la robustesse) + +// BuildRequestPayload contient les informations pour démarrer un build. +type BuildRequestPayload struct { + // Le BuildSpec peut être volumineux, l'envoyer en YAML est souvent pratique. + BuildSpecYAML string `json:"build_spec_yaml"` + // Ou vous pourriez envoyer la struct BuildSpec directement si elle est sérialisable en JSON + // BuildSpec build.BuildSpec `json:"build_spec"` +} + +// BuildQueuedPayload confirme la réception et fournit un ID de build. +type BuildQueuedPayload struct { + BuildID string `json:"build_id"` // ID unique assigné par le serveur pour ce build + Message string `json:"message"` // e.g., "Build job accepted and queued" +} + +// LogChunkPayload contient un morceau de log. +type LogChunkPayload struct { + BuildID string `json:"build_id"` // ID du build concerné + Stream string `json:"stream"` // "stdout" ou "stderr" (ou "system") + Content string `json:"content"` // Le contenu du log + // Sequence int `json:"sequence,omitempty"` // Optionnel: pour garantir l'ordre si nécessaire +} + +// BuildStatusPayload donne l'état actuel du build. +type BuildStatusPayload struct { + BuildID string `json:"build_id"` + Status string `json:"status"` // e.g., "queued", "fetching", "building", "success", "failure" + Message string `json:"message,omitempty"` // Message additionnel (e.g., raison de l'échec) + ArtifactRef string `json:"artifact_ref,omitempty"` // Référence à l'artefact (URL, path B2, tag Docker, etc.) si succès + DurationSec *float64 `json:"duration_sec,omitempty"` // Durée du build si terminé + // On évite d'envoyer le BuildResult complet ici, trop lourd potentiellement. +} + +// SecretRequestPayload demande une valeur de secret. +type SecretRequestPayload struct { + Source string `json:"source"` // L'identifiant du secret requis (correspond à SecretSpec.Source) +} + +// SecretResponsePayload fournit la valeur d'un secret. +type SecretResponsePayload struct { + Source string `json:"source"` // L'identifiant demandé + Value string `json:"value"` // La valeur du secret (ATTENTION: Sécurité !) +} + +// ErrorPayload (utilisé si Message.Type == EvtError) +type ErrorPayload struct { + Code int `json:"code,omitempty"` // Code d'erreur interne (optionnel) + Details string `json:"details"` // Description de l'erreur +} + +// --- Helpers pour créer des messages --- + +// NewMessage crée un message simple sans payload. +func NewMessage(eventType EventType, requestID string) *Message { + return &Message{Type: eventType, RequestID: requestID} +} + +// NewErrorMessage crée un message d'erreur. +func NewErrorMessage(requestID, errMsg string, details string) *Message { + payloadBytes, _ := json.Marshal(ErrorPayload{Details: details}) + return &Message{ + Type: EvtError, + RequestID: requestID, + Payload: payloadBytes, + Error: errMsg, + } +} + +// AddPayload attache un payload structuré à un message existant. +func (m *Message) AddPayload(payload interface{}) error { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload for type %s: %w", m.Type, err) + } + m.Payload = payloadBytes + return nil +} + +// DecodePayload décode le Payload json.RawMessage dans la structure fournie. +func (m *Message) DecodePayload(target interface{}) error { + if len(m.Payload) == 0 { + return fmt.Errorf("message payload is empty for type %s", m.Type) + } + if err := json.Unmarshal(m.Payload, target); err != nil { + return fmt.Errorf("failed to unmarshal payload for type %s: %w", m.Type, err) + } + return nil +} \ No newline at end of file diff --git a/socket/server.go b/socket/server.go new file mode 100644 index 0000000..ee3c3d5 --- /dev/null +++ b/socket/server.go @@ -0,0 +1,267 @@ +// socket/server.go +package socket + +import ( + "context" + "fmt" + "log" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// Server gère les connexions WebSocket entrantes. +type Server struct { + hub *Hub + upgrader websocket.Upgrader + // Ajouter ici les dépendances nécessaires au handler métier + buildService BuildTriggerer // Interface pour découpler du package build + secretFetcher SecretFetcher // Interface pour découpler +} + +// BuildTriggerer définit l'interface pour démarrer un build et obtenir des notifications. +// Ceci permet de ne pas dépendre directement du package build concret. +type BuildTriggerer interface { + // StartBuildAsync démarre un build et retourne immédiatement un buildID. + // Il prend un notificateur pour renvoyer les logs et le statut final. + StartBuildAsync(ctx context.Context, buildID string, buildSpecYAML string, notifier BuildNotifier) error +} + +// SecretFetcher définit l'interface pour récupérer des secrets. +type SecretFetcher interface { + GetSecret(ctx context.Context, source string) (string, error) +} + +// BuildNotifier est utilisé par le service de build pour envoyer des mises à jour au serveur socket. +type BuildNotifier interface { + NotifyLog(buildID string, stream string, content string) + NotifyStatus(buildID, status, artifactRef string, buildErr error, duration *float64) +} + +// serverBuildNotifier implémente BuildNotifier et envoie les notifications via le Hub. +type serverBuildNotifier struct { + hub *Hub + buildToClient map[string]*connection // Map pour savoir à quel client envoyer les notifs + mu sync.RWMutex // Protéger la map +} + +func newServerBuildNotifier(hub *Hub) *serverBuildNotifier { + return &serverBuildNotifier{ + hub: hub, + buildToClient: make(map[string]*connection), + } +} + +// Associer un build ID à une connexion client spécifique +func (sbn *serverBuildNotifier) registerBuildClient(buildID string, clientConn *connection) { + sbn.mu.Lock() + defer sbn.mu.Unlock() + sbn.buildToClient[buildID] = clientConn + log.Printf("Notifier: Registered client %p for build %s\n", clientConn.ws, buildID) +} + +// Désassocier un build ID (quand le build est fini ou le client déconnecté) +func (sbn *serverBuildNotifier) unregisterBuild(buildID string) { + sbn.mu.Lock() + defer sbn.mu.Unlock() + delete(sbn.buildToClient, buildID) + log.Printf("Notifier: Unregistered build %s\n", buildID) +} + +func (sbn *serverBuildNotifier) getClientForBuild(buildID string) *connection { + sbn.mu.RLock() + defer sbn.mu.RUnlock() + return sbn.buildToClient[buildID] +} + +func (sbn *serverBuildNotifier) NotifyLog(buildID string, stream string, content string) { + clientConn := sbn.getClientForBuild(buildID) + if clientConn == nil { + log.Printf("Notifier: No client found for build %s to send log chunk.\n", buildID) + return + } + + msg := NewMessage(EvtLogChunk, "") // Pas de requestID pour les logs streamés + payload := LogChunkPayload{ + BuildID: buildID, + Stream: stream, + Content: content, + } + if err := msg.AddPayload(payload); err == nil { + clientConn.sendMsg(msg) + } else { + log.Printf("Notifier: Error creating log chunk payload for build %s: %v\n", buildID, err) + } +} + +func (sbn *serverBuildNotifier) NotifyStatus(buildID string, status string, artifactRef string, buildErr error, duration *float64) { + clientConn := sbn.getClientForBuild(buildID) + if clientConn == nil { + log.Printf("Notifier: No client found for build %s to send status update.\n", buildID) + // On doit quand même désenregistrer le build pour éviter les fuites + sbn.unregisterBuild(buildID) + return + } + + msg := NewMessage(EvtBuildStatus, "") + payload := BuildStatusPayload{ + BuildID: buildID, + Status: status, + ArtifactRef: artifactRef, + DurationSec: duration, + } + if buildErr != nil { + payload.Message = buildErr.Error() // Ajouter le message d'erreur au statut + } + + if err := msg.AddPayload(payload); err == nil { + clientConn.sendMsg(msg) + } else { + log.Printf("Notifier: Error creating build status payload for build %s: %v\n", buildID, err) + } + + // Si le build est terminé (succès ou échec), désenregistrer + if status == "success" || status == "failure" { + sbn.unregisterBuild(buildID) + } +} + + +// NewServer crée une nouvelle instance de serveur WebSocket. +func NewServer(buildSvc BuildTriggerer, secretF SecretFetcher) *Server { + server := &Server{ + // Configurer l'upgrader (vérifier l'origine, etc.) + upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // CheckOrigin est important pour la sécurité en production ! + CheckOrigin: func(r *http.Request) bool { + // Mettre ici la logique pour autoriser les origines (ex: localhost, votre domaine CLI/UI) + // Pour le développement, on peut autoriser tout : + log.Printf("CheckOrigin: Allowing origin %s\n", r.Header.Get("Origin")) + return true // ATTENTION: Trop permissif pour la prod + }, + }, + buildService: buildSvc, + secretFetcher: secretF, + } + // Le hub a besoin du handler de message défini dans le serveur + server.hub = newHub(server.handleMessage) + return server +} + +// Run démarre le hub dans une goroutine. +func (s *Server) Run() { + go s.hub.run() +} + +// ServeHTTP gère les requêtes HTTP et tente de les upgrader en WebSocket. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Upgrade la connexion HTTP en WebSocket + ws, err := s.upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("ServeHTTP: Failed to upgrade connection: %v\n", err) + // L'upgrader envoie déjà une réponse HTTP d'erreur + return + } + log.Printf("ServeHTTP: Client connected from %s\n", ws.RemoteAddr()) + + // Crée une nouvelle connexion gérée + conn := newConnection(ws) + + // Enregistre le client auprès du hub + s.hub.register <- conn + + // Démarre les pompes de lecture et d'écriture pour cette connexion + // La readPump appellera hub.handleDisconnect quand elle se terminera. + go conn.writePump() + go conn.readPump(s.hub.handleIncomingMessage, s.hub.handleDisconnect) +} + +// handleMessage est le point central de traitement des messages entrants du serveur. +func (s *Server) handleMessage(msg *Message, client *connection) error { + ctx := context.Background() // Utiliser un contexte approprié + log.Printf("Server: Handling message type '%s' from %p (ReqID: %s)\n", msg.Type, client.ws, msg.RequestID) + + switch msg.Type { + case EvtBuildRequest: + var payload BuildRequestPayload + if err := msg.DecodePayload(&payload); err != nil { + return fmt.Errorf("invalid build request payload: %w", err) + } + if payload.BuildSpecYAML == "" { + return fmt.Errorf("build spec YAML cannot be empty") + } + + // Générer un ID de build unique + buildID := fmt.Sprintf("build-%d", time.Now().UnixNano()) // Ou utiliser UUID + + // Accusé de réception immédiat au client + ackPayload := BuildQueuedPayload{BuildID: buildID, Message: "Build job accepted"} + ackMsg := NewMessage(EvtBuildQueued, msg.RequestID) // Utilise le RequestID original + if err := ackMsg.AddPayload(ackPayload); err != nil { + log.Printf("Server: Failed to create build queued payload: %v\n", err) + // Continuer quand même à lancer le build + } + client.sendMsg(ackMsg) // Envoyer l'ack + + // Créer et enregistrer le notificateur pour ce build + notifier := newServerBuildNotifier(s.hub) // TODO: Avoir un seul notificateur partagé ? Ou un par build ? Un partagé semble mieux. + notifier.registerBuildClient(buildID, client) // Associer le client à ce build + + // Démarrer le build de manière asynchrone via l'interface + go func() { + log.Printf("Server: Starting build %s asynchronously\n", buildID) + // Le contexte peut être utilisé pour l'annulation éventuelle + err := s.buildService.StartBuildAsync(context.Background(), buildID, payload.BuildSpecYAML, notifier) + if err != nil { + // Si StartBuildAsync échoue immédiatement (rare), notifier l'échec + log.Printf("Server: Failed to start build %s: %v\n", buildID, err) + notifier.NotifyStatus(buildID, "failure", "", err, nil) + // Le notifier va désenregistrer le build + } + // Si StartBuildAsync réussit, le build s'exécute et le notifier gèrera les logs/status + }() + + return nil // Succès du traitement de la requête (le build est lancé en async) + + case EvtSecretRequest: + var payload SecretRequestPayload + if err := msg.DecodePayload(&payload); err != nil { + return fmt.Errorf("invalid secret request payload: %w", err) + } + if payload.Source == "" { + return fmt.Errorf("secret source cannot be empty") + } + if s.secretFetcher == nil { + return fmt.Errorf("secret fetcher service is not configured on the server") + } + + // Récupérer le secret (synchrone ici, pourrait être async) + secretValue, err := s.secretFetcher.GetSecret(ctx, payload.Source) + if err != nil { + // Envoyer une réponse d'erreur spécifique au secret + errMsg := NewErrorMessage(msg.RequestID, "Failed to fetch secret", err.Error()) + client.sendMsg(errMsg) + return nil // L'erreur a été gérée en envoyant un message d'erreur + } + + // Envoyer la réponse avec le secret + respPayload := SecretResponsePayload{Source: payload.Source, Value: secretValue} + respMsg := NewMessage(EvtSecretResponse, msg.RequestID) + if err := respMsg.AddPayload(respPayload); err != nil { + return fmt.Errorf("failed to create secret response payload: %w", err) // Erreur interne serveur + } + client.sendMsg(respMsg) + return nil + + default: + log.Printf("Server: Received unhandled message type '%s'\n", msg.Type) + // Envoyer une erreur au client pour indiquer que le type n'est pas géré ? + errMsg := NewErrorMessage(msg.RequestID, "Unhandled message type", fmt.Sprintf("Type '%s' not supported by server", msg.Type)) + client.sendMsg(errMsg) + return nil // Pas une erreur fatale pour la connexion + } +} \ No newline at end of file diff --git a/socket/socket_test.go b/socket/socket_test.go new file mode 100644 index 0000000..e892445 --- /dev/null +++ b/socket/socket_test.go @@ -0,0 +1,214 @@ +package socket + +import ( + "context" + "fmt" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- Mocks pour BuildTriggerer et SecretFetcher --- + +type MockBuildTriggerer struct { + StartBuildFunc func(ctx context.Context, buildID string, buildSpecYAML string, notifier BuildNotifier) error +} + +func (m *MockBuildTriggerer) StartBuildAsync(ctx context.Context, buildID string, buildSpecYAML string, notifier BuildNotifier) error { + if m.StartBuildFunc != nil { + return m.StartBuildFunc(ctx, buildID, buildSpecYAML, notifier) + } + return fmt.Errorf("StartBuildFunc not implemented in mock") +} + +type MockSecretFetcher struct { + GetSecretFunc func(ctx context.Context, source string) (string, error) +} + +func (m *MockSecretFetcher) GetSecret(ctx context.Context, source string) (string, error) { + if m.GetSecretFunc != nil { + return m.GetSecretFunc(ctx, source) + } + return "", fmt.Errorf("GetSecretFunc not implemented in mock") +} + +// --- Test Client-Server Interaction --- + +func TestSocket_ClientServerCommunication(t *testing.T) { + // 1. Setup Mock Services + var wg sync.WaitGroup // Pour attendre les goroutines de notification + var receivedBuildID string + var receivedBuildSpec string + + mockBuildSvc := &MockBuildTriggerer{ + StartBuildFunc: func(ctx context.Context, buildID string, buildSpecYAML string, notifier BuildNotifier) error { + t.Logf("MockBuildTriggerer: StartBuildAsync called for BuildID: %s\n", buildID) + receivedBuildID = buildID // Capturer pour vérification + receivedBuildSpec = buildSpecYAML + + // Simuler un build en arrière-plan qui envoie des logs et un statut + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(50 * time.Millisecond) // Simuler le travail + notifier.NotifyLog(buildID, "stdout", "Fetching code...") + time.Sleep(50 * time.Millisecond) + notifier.NotifyLog(buildID, "stdout", "Building image...") + time.Sleep(50 * time.Millisecond) + // Simuler un succès + duration := 150.0 * time.Millisecond.Seconds() + notifier.NotifyStatus(buildID, "success", "docker.io/library/test:latest", nil, &duration) + t.Logf("MockBuildTriggerer: Sent final status for BuildID: %s\n", buildID) + }() + return nil // Retourner nil pour indiquer que le lancement async a réussi + }, + } + + mockSecretSvc := &MockSecretFetcher{ + GetSecretFunc: func(ctx context.Context, source string) (string, error) { + t.Logf("MockSecretFetcher: GetSecret called for source: %s\n", source) + if source == "valid/secret" { + return "secret_value_123", nil + } + return "", fmt.Errorf("secret '%s' not found", source) + }, + } + + // 2. Start Test Server + server := NewServer(mockBuildSvc, mockSecretSvc) + server.Run() // Démarre le hub + + httpServer := httptest.NewServer(server) // Utilise le ServeHTTP du serveur socket + defer httpServer.Close() + + // Convertir l'URL HTTP en WS + wsURL := "ws" + strings.TrimPrefix(httpServer.URL, "http") + t.Logf("Test WebSocket Server running at: %s\n", wsURL) + + // 3. Start Client and Connect + client := NewClient() + err := client.Connect(wsURL, nil) + require.NoError(t, err, "Client failed to connect") + defer client.Close() // S'assurer que le client est fermé + + require.True(t, client.IsConnected(), "Client should be connected") + + // 4. Test Build Request / Response Flow + buildMessagesReceived := make(chan *Message, 10) // Buffer pour collecter les messages liés au build + go func() { + for msg := range client.Incoming { + // Filtrer les messages pour ce test + if msg.Type == EvtBuildQueued || msg.Type == EvtLogChunk || msg.Type == EvtBuildStatus { + buildMessagesReceived <- msg + } else { + t.Logf("Client Incoming Monitor: Received other message type: %s\n", msg.Type) + } + } + t.Log("Client Incoming Monitor: Exited.") + close(buildMessagesReceived) // Fermer quand client.Incoming est fermé + }() + + // Envoyer la requête de build + buildSpecContent := "name: test-build\nversion: '1.0'\n..." // Contenu YAML factice + buildReqPayload := BuildRequestPayload{BuildSpecYAML: buildSpecContent} + ctxReq, cancelReq := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelReq() + + // Utiliser SendRequest pour obtenir l'accusé de réception (BuildQueued) + respMsg, err := client.SendRequest(ctxReq, EvtBuildRequest, buildReqPayload) + require.NoError(t, err, "SendRequest for build failed") + require.NotNil(t, respMsg, "Response message should not be nil") + require.Equal(t, EvtBuildQueued, respMsg.Type, "Response should be BuildQueued") + + // Décoder le payload de la réponse pour obtenir le buildID + var queuedPayload BuildQueuedPayload + err = respMsg.DecodePayload(&queuedPayload) + require.NoError(t, err) + require.NotEmpty(t, queuedPayload.BuildID, "BuildID should be in queued payload") + assert.Equal(t, queuedPayload.BuildID, receivedBuildID, "BuildID in response should match ID received by mock service") // Vérifier la cohérence + assert.Equal(t, buildSpecContent, receivedBuildSpec, "BuildSpec received by mock should match sent spec") + + // Attendre les messages streamés (logs, status final) + expectedLogs := []string{"Fetching code...", "Building image..."} + receivedLogs := []string{} + var finalStatusPayload BuildStatusPayload + receivedFinalStatus := false + + timeout := time.After(3 * time.Second) // Timeout pour attendre les messages streamés + logLoopDone := false + for !logLoopDone { + select { + case msg, ok := <-buildMessagesReceived: + if !ok { + t.Log("Build message channel closed.") + logLoopDone = true + break // Sortir si le canal est fermé + } + t.Logf("Client Received Async Message: Type=%s", msg.Type) + switch msg.Type { + case EvtLogChunk: + var logPayload LogChunkPayload + err := msg.DecodePayload(&logPayload) + require.NoError(t, err) + assert.Equal(t, receivedBuildID, logPayload.BuildID) + receivedLogs = append(receivedLogs, logPayload.Content) + case EvtBuildStatus: + err := msg.DecodePayload(&finalStatusPayload) + require.NoError(t, err) + assert.Equal(t, receivedBuildID, finalStatusPayload.BuildID) + receivedFinalStatus = true + logLoopDone = true // On a reçu le statut final + } + case <-timeout: + t.Fatal("Timeout waiting for streamed build messages (logs/status)") + } + } + + // Vérifier les logs et le statut final reçus + assert.ElementsMatch(t, expectedLogs, receivedLogs, "Received logs do not match expected logs") + require.True(t, receivedFinalStatus, "Should have received a final build status") + assert.Equal(t, "success", finalStatusPayload.Status) + assert.Equal(t, "docker.io/library/test:latest", finalStatusPayload.ArtifactRef) + require.NotNil(t, finalStatusPayload.DurationSec) + assert.True(t, *finalStatusPayload.DurationSec > 0) + + // 5. Test Secret Request / Response Flow + secretReqPayload := SecretRequestPayload{Source: "valid/secret"} + ctxSecret, cancelSecret := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelSecret() + + secretRespMsg, err := client.SendRequest(ctxSecret, EvtSecretRequest, secretReqPayload) + require.NoError(t, err, "SendRequest for secret failed") + require.NotNil(t, secretRespMsg) + require.Equal(t, EvtSecretResponse, secretRespMsg.Type) + + var secretRespPayload SecretResponsePayload + err = secretRespMsg.DecodePayload(&secretRespPayload) + require.NoError(t, err) + assert.Equal(t, "valid/secret", secretRespPayload.Source) + assert.Equal(t, "secret_value_123", secretRespPayload.Value) + + // Test secret non trouvé + secretReqPayloadFail := SecretRequestPayload{Source: "invalid/secret"} + ctxSecretFail, cancelSecretFail := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelSecretFail() + + _, err = client.SendRequest(ctxSecretFail, EvtSecretRequest, secretReqPayloadFail) + require.Error(t, err, "SendRequest for invalid secret should fail") + assert.Contains(t, err.Error(), "secret 'invalid/secret' not found") // Vérifier l'erreur du serveur renvoyée + + // Attendre que la goroutine de notification du mock se termine + wg.Wait() + t.Log("Mock Build goroutine finished.") + + // Fermer explicitement le client pour terminer le moniteur Incoming + client.Close() + // Attendre un court instant pour que le canal `buildMessagesReceived` se ferme + <-time.After(100 * time.Millisecond) + +} \ No newline at end of file From 2df3eaaf4eb528128566b489750bfe5d5cb2c6a9 Mon Sep 17 00:00:00 2001 From: DoniLite Date: Fri, 1 Aug 2025 10:13:02 +0000 Subject: [PATCH 6/8] chore(workspace): refactor architecture and move unused modules --- builder/build/builder.go | 143 -- builder/go.mod | 7 - builder/go.sum | 4 - builder/main.go | 14 - builder/watch/main.go | 19 - bx/build/build_integration_test.go | 2 +- bx/build/socket.go | 2 +- bx/cmd/run.go | 6 +- bx/go.mod | 2 +- cli/go.mod | 2 +- frontend/package.json | 24 - frontend/pnpm-lock.yaml | 1336 ----------------- frontend/src/components/App.tsx | 8 - frontend/src/components/Header.tsx | 5 - frontend/src/components/partials/HomeView.tsx | 38 - frontend/src/generated.d.ts | 6 - frontend/src/input.css | 7 - frontend/src/pages/Home.tsx | 15 - frontend/src/store.ts | 19 - frontend/tsconfig.json | 19 - frontend/vite.config.ts | 21 - server/api/routes.go | 27 - server/atlas.hcl | 21 - server/controllers/auth_controller.go | 64 - server/controllers/file_controller.go | 34 - server/controllers/test_controller.go | 7 - server/db/db.go | 38 - server/default.pgo | Bin 450 -> 0 bytes server/go.mod | 81 - server/go.sum | 284 ---- server/loader/main.go | 20 - server/main.go | 52 - server/middleware/auth.go | 38 - server/migrations/20241231112547.sql | 28 - server/migrations/atlas.sum | 2 - server/models/models.go | 24 - server/models/props.go | 5 - server/routes/main.go | 20 - server/routes/static.go | 25 - server/server_test.go | 24 - services/builder_service.go | 25 - services/cloud_service.go | 66 - services/compression_service.go | 40 - services/encryption_service.go | 25 - services/go.mod | 37 - services/go.sum | 139 -- services/init_service.go | 32 - services/secureAlgo_service.go | 616 -------- services/services_test.go | 59 - socket/go.mod | 2 +- utils/go.mod | 3 - utils/helpers.go | 70 - utils/helpers_test.go | 20 - 53 files changed, 8 insertions(+), 3619 deletions(-) delete mode 100644 builder/build/builder.go delete mode 100644 builder/go.mod delete mode 100644 builder/go.sum delete mode 100644 builder/main.go delete mode 100644 builder/watch/main.go delete mode 100644 frontend/package.json delete mode 100644 frontend/pnpm-lock.yaml delete mode 100644 frontend/src/components/App.tsx delete mode 100644 frontend/src/components/Header.tsx delete mode 100644 frontend/src/components/partials/HomeView.tsx delete mode 100644 frontend/src/generated.d.ts delete mode 100644 frontend/src/input.css delete mode 100644 frontend/src/pages/Home.tsx delete mode 100644 frontend/src/store.ts delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts delete mode 100644 server/api/routes.go delete mode 100644 server/atlas.hcl delete mode 100644 server/controllers/auth_controller.go delete mode 100644 server/controllers/file_controller.go delete mode 100644 server/controllers/test_controller.go delete mode 100644 server/db/db.go delete mode 100644 server/default.pgo delete mode 100644 server/go.mod delete mode 100644 server/go.sum delete mode 100644 server/loader/main.go delete mode 100644 server/main.go delete mode 100644 server/middleware/auth.go delete mode 100644 server/migrations/20241231112547.sql delete mode 100644 server/migrations/atlas.sum delete mode 100644 server/models/models.go delete mode 100644 server/models/props.go delete mode 100644 server/routes/main.go delete mode 100644 server/routes/static.go delete mode 100644 server/server_test.go delete mode 100644 services/builder_service.go delete mode 100644 services/cloud_service.go delete mode 100644 services/compression_service.go delete mode 100644 services/encryption_service.go delete mode 100644 services/go.mod delete mode 100644 services/go.sum delete mode 100644 services/init_service.go delete mode 100644 services/secureAlgo_service.go delete mode 100644 services/services_test.go delete mode 100644 utils/go.mod delete mode 100644 utils/helpers.go delete mode 100644 utils/helpers_test.go diff --git a/builder/build/builder.go b/builder/build/builder.go deleted file mode 100644 index f8c4847..0000000 --- a/builder/build/builder.go +++ /dev/null @@ -1,143 +0,0 @@ -package build - -import ( - "fmt" - "log" - "os" - "os/signal" - "path/filepath" - "regexp" - "syscall" - - "github.com/evanw/esbuild/pkg/api" -) - -// Créer un plugin pour modifier les imports -func createImportPlugin() api.Plugin { - return api.Plugin{ - Name: "import-extension", - Setup: func(build api.PluginBuild) { - build.OnLoad(api.OnLoadOptions{ - Filter: `\.ts$`, // Cibler uniquement les fichiers TypeScript - }, func(args api.OnLoadArgs) (api.OnLoadResult, error) { - // Lire le contenu du fichier actuel - content, err := os.ReadFile(args.Path) - if err != nil { - return api.OnLoadResult{}, err - } - - // Modifier les imports relatifs pour ajouter ".js" - updatedContent := regexp.MustCompile(`from\s+(['"])(\.{1,2}/[^'"]*)(['"])`). - ReplaceAllString(string(content), `from $1$2.js$1`) - - return api.OnLoadResult{ - Contents: &updatedContent, // Contenu modifié - Loader: api.LoaderTS, // Garder le loader TypeScript - ResolveDir: filepath.Dir(args.Path), // Répertoire pour la résolution - }, nil - }) - }, - } -} - -func buildTSFile(inputPath string) error { - outputPath := filepath.Join( - "./client/js", - fmt.Sprintf("%s.js", inputPath[:len(inputPath)-3]), - ) - - result := api.Build(api.BuildOptions{ - EntryPoints: []string{inputPath}, - Bundle: false, - Write: true, - Outfile: outputPath, - Format: api.FormatESModule, - Target: api.ES2015, - Sourcemap: api.SourceMapLinked, - Platform: api.PlatformBrowser, - Loader: map[string]api.Loader{ - ".ts": api.LoaderTS, // Indiquer à esbuild de gérer les fichiers TypeScript - }, - Plugins: []api.Plugin{createImportPlugin()}, - }) - - if len(result.Errors) > 0 { - return fmt.Errorf("build error: %v", result.Errors) - } - - return nil -} - -func BuildAllTSFiles(sourceDir string) error { - return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if filepath.Ext(path) == ".ts" { - if err := buildTSFile(path); err != nil { - return err - } - } - return nil - }) -} - -// Fonction pour surveiller les fichiers TypeScript dans un dossier -func WatchTSFiles(sourceDir string) error { - // Utiliser doublestar.Glob pour trouver tous les fichiers TypeScript dans le dossier source - var files []string - - err := filepath.WalkDir(sourceDir, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if filepath.Ext(path) == ".ts" { - files = append(files, path) - } - return nil - }) - fmt.Println("Fichiers trouvés :", files) - if err != nil { - log.Fatalf("Erreur lors du parcours des fichiers : %v", err) - } - if err != nil { - return fmt.Errorf("failed to retrieve files: %v", err) - } - if len(files) == 0 { - return fmt.Errorf("no TypeScript files found in directory: %s", sourceDir) - } - - // Créer le contexte de build avec esbuild - ctx, err2 := api.Context(api.BuildOptions{ - EntryPoints: files, - Bundle: false, - Write: true, - Format: api.FormatESModule, - Target: api.ES2015, - Sourcemap: api.SourceMapLinked, - Outdir: "./client/js", - Platform: api.PlatformBrowser, - Plugins: []api.Plugin{createImportPlugin()}, - Loader: map[string]api.Loader{ - ".ts": api.LoaderTS, // Indiquer à esbuild de gérer les fichiers TypeScript - }, - ResolveExtensions: []string{".ts", ".js"}, - }) - if err2 != nil { - return fmt.Errorf("failed to create build context: %v", err) - } - defer ctx.Dispose() - - // Activer le mode surveillance - err = ctx.Watch(api.WatchOptions{}) - if err != nil { - return fmt.Errorf("failed to start watch mode: %v", err) - } - - // Gestion des signaux pour arrêter proprement la surveillance - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop - - return nil -} diff --git a/builder/go.mod b/builder/go.mod deleted file mode 100644 index e787525..0000000 --- a/builder/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module anexis/builder - -go 1.24.2 - -require github.com/evanw/esbuild v0.25.2 - -require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/builder/go.sum b/builder/go.sum deleted file mode 100644 index ce57ce3..0000000 --- a/builder/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/evanw/esbuild v0.25.2 h1:ublSEmZSjzOc6jLO1OTQy/vHc1wiqyDF4oB3hz5sM6s= -github.com/evanw/esbuild v0.25.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/builder/main.go b/builder/main.go deleted file mode 100644 index 954439a..0000000 --- a/builder/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "fmt" - - "anexis/builder/build" -) - -func main() { - err := build.BuildAllTSFiles("./src") - if err != nil { - fmt.Printf("failed to build files: %v", err) - } -} diff --git a/builder/watch/main.go b/builder/watch/main.go deleted file mode 100644 index 3c88ced..0000000 --- a/builder/watch/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - "path/filepath" - - "anexis/builder/build" -) - -func main() { - filePath, err := filepath.Abs("./src") - if err != nil { - fmt.Printf("failed to get absolute path: %v", err) - } - err2 := build.WatchTSFiles(filePath) - if err2 != nil { - fmt.Printf("failed to watch files: %v", err2) - } -} diff --git a/bx/build/build_integration_test.go b/bx/build/build_integration_test.go index 78d372c..fed94eb 100644 --- a/bx/build/build_integration_test.go +++ b/bx/build/build_integration_test.go @@ -12,7 +12,7 @@ import ( "time" // Importer socket et testify etc. - "anexis/socket" // Ajuster le chemin si besoin + "github.com/Treefle-labs/Anexis/socket" // Ajuster le chemin si besoin "github.com/docker/docker/client" "github.com/stretchr/testify/assert" diff --git a/bx/build/socket.go b/bx/build/socket.go index 145d978..c73ffb9 100644 --- a/bx/build/socket.go +++ b/bx/build/socket.go @@ -14,7 +14,7 @@ import ( "time" // Importer le package socket (ajuster le chemin si nécessaire) - "anexis/socket" + "github.com/Treefle-labs/Anexis/socket" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/jsonmessage" // Nécessaire pour le writer de logs diff --git a/bx/cmd/run.go b/bx/cmd/run.go index 00ecf6e..4231a13 100644 --- a/bx/cmd/run.go +++ b/bx/cmd/run.go @@ -10,8 +10,8 @@ import ( "strings" "time" // Pour docker load - "anexis/bx/build" - "anexis/socket" // Pour parser RunYAML + "github.com/Treefle-labs/Anexis/bx/build" + "github.com/Treefle-labs/Anexis/socket" // Pour parser RunYAML "github.com/spf13/cobra" "gopkg.in/yaml.v3" @@ -54,7 +54,7 @@ func runRunCommand(cmd *cobra.Command, args []string) error { return fmt.Errorf("erreur lors de la lecture de '%s': %w", runFile, err) } - var runConfig build.Bui + var runConfig build.RunYAML err = yaml.Unmarshal(runData, &runConfig) if err != nil { return fmt.Errorf("erreur lors du parsing YAML de '%s': %w", runFile, err) diff --git a/bx/go.mod b/bx/go.mod index 7e235d3..95b0d81 100644 --- a/bx/go.mod +++ b/bx/go.mod @@ -1,4 +1,4 @@ -module anexis/bx +module github.com/Treefle-labs/Anexis/bx go 1.24.2 diff --git a/cli/go.mod b/cli/go.mod index 6dc8ac5..42ab7b6 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,3 +1,3 @@ -module anexis/cli +module github.com/Treefle-labs/Anexis/cli go 1.24.2 diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index a0b5a43..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "anexis", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@9.12.0+sha512.4abf725084d7bcbafbd728bfc7bee61f2f791f977fd87542b3579dcb23504d170d46337945e4c66485cd12d588a0c0e570ed9c477e7ccdd8507cf05f3f92eaca", - "dependencies": { - "@tailwindcss/vite": "^4.1.3", - "@types/node": "^22.14.1", - "@types/react": "^19.1.1", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.3.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "tailwindcss": "^4.1.3", - "vite": "^6.2.6" - } -} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml deleted file mode 100644 index 5773c26..0000000 --- a/frontend/pnpm-lock.yaml +++ /dev/null @@ -1,1336 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@tailwindcss/vite': - specifier: ^4.1.3 - version: 4.1.3(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1)) - '@types/node': - specifier: ^22.14.1 - version: 22.14.1 - '@types/react': - specifier: ^19.1.1 - version: 19.1.1 - '@types/react-dom': - specifier: ^19.1.2 - version: 19.1.2(@types/react@19.1.1) - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1)) - react: - specifier: ^19.1.0 - version: 19.1.0 - react-dom: - specifier: ^19.1.0 - version: 19.1.0(react@19.1.0) - tailwindcss: - specifier: ^4.1.3 - version: 4.1.3 - vite: - specifier: ^6.2.6 - version: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.26.8': - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.26.10': - resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.27.0': - resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.0': - resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.0': - resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} - engines: {node: '>=6.9.0'} - - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@rollup/rollup-android-arm-eabi@4.40.0': - resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.40.0': - resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.40.0': - resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.40.0': - resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.40.0': - resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.40.0': - resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': - resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.40.0': - resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.40.0': - resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.40.0': - resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': - resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': - resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.40.0': - resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.40.0': - resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.40.0': - resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.40.0': - resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.40.0': - resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.40.0': - resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.40.0': - resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.40.0': - resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} - cpu: [x64] - os: [win32] - - '@tailwindcss/node@4.1.3': - resolution: {integrity: sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==} - - '@tailwindcss/oxide-android-arm64@4.1.3': - resolution: {integrity: sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.3': - resolution: {integrity: sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.3': - resolution: {integrity: sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.3': - resolution: {integrity: sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3': - resolution: {integrity: sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.3': - resolution: {integrity: sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.3': - resolution: {integrity: sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.3': - resolution: {integrity: sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-linux-x64-musl@4.1.3': - resolution: {integrity: sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.3': - resolution: {integrity: sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.3': - resolution: {integrity: sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.3': - resolution: {integrity: sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==} - engines: {node: '>= 10'} - - '@tailwindcss/vite@4.1.3': - resolution: {integrity: sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==} - peerDependencies: - vite: ^5.2.0 || ^6 - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - - '@types/node@22.14.1': - resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} - - '@types/react-dom@19.1.2': - resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} - peerDependencies: - '@types/react': ^19.0.0 - - '@types/react@19.1.1': - resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==} - - '@vitejs/plugin-react@4.3.4': - resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - caniuse-lite@1.0.30001713: - resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - electron-to-chromium@1.5.136: - resolution: {integrity: sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==} - - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} - engines: {node: '>=10.13.0'} - - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - lightningcss-darwin-arm64@1.29.2: - resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.29.2: - resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.29.2: - resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.29.2: - resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.29.2: - resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-arm64-musl@1.29.2: - resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - - lightningcss-linux-x64-gnu@1.29.2: - resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-linux-x64-musl@1.29.2: - resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - - lightningcss-win32-arm64-msvc@1.29.2: - resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.29.2: - resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.29.2: - resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} - engines: {node: '>= 12.0.0'} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - - react-dom@19.1.0: - resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} - peerDependencies: - react: ^19.1.0 - - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} - engines: {node: '>=0.10.0'} - - rollup@4.40.0: - resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - tailwindcss@4.1.3: - resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==} - - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - vite@6.2.6: - resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yaml@2.6.1: - resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} - engines: {node: '>= 14'} - hasBin: true - -snapshots: - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.26.8': {} - - '@babel/core@7.26.10': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 - convert-source-map: 2.0.0 - debug: 4.4.0 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.27.0': - dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.0': - dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-module-imports@7.25.9': - dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.25.9': {} - - '@babel/helpers@7.27.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/parser@7.27.0': - dependencies: - '@babel/types': 7.27.0 - - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/template@7.27.0': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/traverse@7.27.0': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.27.0': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@esbuild/aix-ppc64@0.25.2': - optional: true - - '@esbuild/android-arm64@0.25.2': - optional: true - - '@esbuild/android-arm@0.25.2': - optional: true - - '@esbuild/android-x64@0.25.2': - optional: true - - '@esbuild/darwin-arm64@0.25.2': - optional: true - - '@esbuild/darwin-x64@0.25.2': - optional: true - - '@esbuild/freebsd-arm64@0.25.2': - optional: true - - '@esbuild/freebsd-x64@0.25.2': - optional: true - - '@esbuild/linux-arm64@0.25.2': - optional: true - - '@esbuild/linux-arm@0.25.2': - optional: true - - '@esbuild/linux-ia32@0.25.2': - optional: true - - '@esbuild/linux-loong64@0.25.2': - optional: true - - '@esbuild/linux-mips64el@0.25.2': - optional: true - - '@esbuild/linux-ppc64@0.25.2': - optional: true - - '@esbuild/linux-riscv64@0.25.2': - optional: true - - '@esbuild/linux-s390x@0.25.2': - optional: true - - '@esbuild/linux-x64@0.25.2': - optional: true - - '@esbuild/netbsd-arm64@0.25.2': - optional: true - - '@esbuild/netbsd-x64@0.25.2': - optional: true - - '@esbuild/openbsd-arm64@0.25.2': - optional: true - - '@esbuild/openbsd-x64@0.25.2': - optional: true - - '@esbuild/sunos-x64@0.25.2': - optional: true - - '@esbuild/win32-arm64@0.25.2': - optional: true - - '@esbuild/win32-ia32@0.25.2': - optional: true - - '@esbuild/win32-x64@0.25.2': - optional: true - - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@rollup/rollup-android-arm-eabi@4.40.0': - optional: true - - '@rollup/rollup-android-arm64@4.40.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.40.0': - optional: true - - '@rollup/rollup-darwin-x64@4.40.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.40.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.40.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.40.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.40.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.40.0': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.40.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.40.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.40.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.40.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.40.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.40.0': - optional: true - - '@tailwindcss/node@4.1.3': - dependencies: - enhanced-resolve: 5.18.1 - jiti: 2.4.2 - lightningcss: 1.29.2 - tailwindcss: 4.1.3 - - '@tailwindcss/oxide-android-arm64@4.1.3': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.3': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.3': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.3': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.3': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.3': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.3': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.3': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.3': - optional: true - - '@tailwindcss/oxide@4.1.3': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.3 - '@tailwindcss/oxide-darwin-arm64': 4.1.3 - '@tailwindcss/oxide-darwin-x64': 4.1.3 - '@tailwindcss/oxide-freebsd-x64': 4.1.3 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.3 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.3 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.3 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.3 - '@tailwindcss/oxide-linux-x64-musl': 4.1.3 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.3 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.3 - - '@tailwindcss/vite@4.1.3(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1))': - dependencies: - '@tailwindcss/node': 4.1.3 - '@tailwindcss/oxide': 4.1.3 - tailwindcss: 4.1.3 - vite: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.27.0 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - - '@types/babel__traverse@7.20.7': - dependencies: - '@babel/types': 7.27.0 - - '@types/estree@1.0.7': {} - - '@types/node@22.14.1': - dependencies: - undici-types: 6.21.0 - - '@types/react-dom@19.1.2(@types/react@19.1.1)': - dependencies: - '@types/react': 19.1.1 - - '@types/react@19.1.1': - dependencies: - csstype: 3.1.3 - - '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1))': - dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1) - transitivePeerDependencies: - - supports-color - - browserslist@4.24.4: - dependencies: - caniuse-lite: 1.0.30001713 - electron-to-chromium: 1.5.136 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.4) - - caniuse-lite@1.0.30001713: {} - - convert-source-map@2.0.0: {} - - csstype@3.1.3: {} - - debug@4.4.0: - dependencies: - ms: 2.1.3 - - detect-libc@2.0.3: {} - - electron-to-chromium@1.5.136: {} - - enhanced-resolve@5.18.1: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - - esbuild@0.25.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.2 - '@esbuild/android-arm': 0.25.2 - '@esbuild/android-arm64': 0.25.2 - '@esbuild/android-x64': 0.25.2 - '@esbuild/darwin-arm64': 0.25.2 - '@esbuild/darwin-x64': 0.25.2 - '@esbuild/freebsd-arm64': 0.25.2 - '@esbuild/freebsd-x64': 0.25.2 - '@esbuild/linux-arm': 0.25.2 - '@esbuild/linux-arm64': 0.25.2 - '@esbuild/linux-ia32': 0.25.2 - '@esbuild/linux-loong64': 0.25.2 - '@esbuild/linux-mips64el': 0.25.2 - '@esbuild/linux-ppc64': 0.25.2 - '@esbuild/linux-riscv64': 0.25.2 - '@esbuild/linux-s390x': 0.25.2 - '@esbuild/linux-x64': 0.25.2 - '@esbuild/netbsd-arm64': 0.25.2 - '@esbuild/netbsd-x64': 0.25.2 - '@esbuild/openbsd-arm64': 0.25.2 - '@esbuild/openbsd-x64': 0.25.2 - '@esbuild/sunos-x64': 0.25.2 - '@esbuild/win32-arm64': 0.25.2 - '@esbuild/win32-ia32': 0.25.2 - '@esbuild/win32-x64': 0.25.2 - - escalade@3.2.0: {} - - fsevents@2.3.3: - optional: true - - gensync@1.0.0-beta.2: {} - - globals@11.12.0: {} - - graceful-fs@4.2.11: {} - - jiti@2.4.2: {} - - js-tokens@4.0.0: {} - - jsesc@3.1.0: {} - - json5@2.2.3: {} - - lightningcss-darwin-arm64@1.29.2: - optional: true - - lightningcss-darwin-x64@1.29.2: - optional: true - - lightningcss-freebsd-x64@1.29.2: - optional: true - - lightningcss-linux-arm-gnueabihf@1.29.2: - optional: true - - lightningcss-linux-arm64-gnu@1.29.2: - optional: true - - lightningcss-linux-arm64-musl@1.29.2: - optional: true - - lightningcss-linux-x64-gnu@1.29.2: - optional: true - - lightningcss-linux-x64-musl@1.29.2: - optional: true - - lightningcss-win32-arm64-msvc@1.29.2: - optional: true - - lightningcss-win32-x64-msvc@1.29.2: - optional: true - - lightningcss@1.29.2: - dependencies: - detect-libc: 2.0.3 - optionalDependencies: - lightningcss-darwin-arm64: 1.29.2 - lightningcss-darwin-x64: 1.29.2 - lightningcss-freebsd-x64: 1.29.2 - lightningcss-linux-arm-gnueabihf: 1.29.2 - lightningcss-linux-arm64-gnu: 1.29.2 - lightningcss-linux-arm64-musl: 1.29.2 - lightningcss-linux-x64-gnu: 1.29.2 - lightningcss-linux-x64-musl: 1.29.2 - lightningcss-win32-arm64-msvc: 1.29.2 - lightningcss-win32-x64-msvc: 1.29.2 - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - ms@2.1.3: {} - - nanoid@3.3.8: {} - - node-releases@2.0.19: {} - - picocolors@1.1.1: {} - - postcss@8.5.3: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - react-dom@19.1.0(react@19.1.0): - dependencies: - react: 19.1.0 - scheduler: 0.26.0 - - react-refresh@0.14.2: {} - - react@19.1.0: {} - - rollup@4.40.0: - dependencies: - '@types/estree': 1.0.7 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.0 - '@rollup/rollup-android-arm64': 4.40.0 - '@rollup/rollup-darwin-arm64': 4.40.0 - '@rollup/rollup-darwin-x64': 4.40.0 - '@rollup/rollup-freebsd-arm64': 4.40.0 - '@rollup/rollup-freebsd-x64': 4.40.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 - '@rollup/rollup-linux-arm-musleabihf': 4.40.0 - '@rollup/rollup-linux-arm64-gnu': 4.40.0 - '@rollup/rollup-linux-arm64-musl': 4.40.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-gnu': 4.40.0 - '@rollup/rollup-linux-riscv64-musl': 4.40.0 - '@rollup/rollup-linux-s390x-gnu': 4.40.0 - '@rollup/rollup-linux-x64-gnu': 4.40.0 - '@rollup/rollup-linux-x64-musl': 4.40.0 - '@rollup/rollup-win32-arm64-msvc': 4.40.0 - '@rollup/rollup-win32-ia32-msvc': 4.40.0 - '@rollup/rollup-win32-x64-msvc': 4.40.0 - fsevents: 2.3.3 - - scheduler@0.26.0: {} - - semver@6.3.1: {} - - source-map-js@1.2.1: {} - - tailwindcss@4.1.3: {} - - tapable@2.2.1: {} - - undici-types@6.21.0: {} - - update-browserslist-db@1.1.3(browserslist@4.24.4): - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 - - vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.6.1): - dependencies: - esbuild: 0.25.2 - postcss: 8.5.3 - rollup: 4.40.0 - optionalDependencies: - '@types/node': 22.14.1 - fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.29.2 - yaml: 2.6.1 - - yallist@3.1.1: {} - - yaml@2.6.1: - optional: true diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx deleted file mode 100644 index e43f1d5..0000000 --- a/frontend/src/components/App.tsx +++ /dev/null @@ -1,8 +0,0 @@ -'use server' - -import { PropsWithChildren } from 'react'; - -type props = PropsWithChildren<{}>; -export default async function App({ children }: props) { - return
{children}
; -} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx deleted file mode 100644 index c0d03b8..0000000 --- a/frontend/src/components/Header.tsx +++ /dev/null @@ -1,5 +0,0 @@ - - -export default function Header() { - return <> -} \ No newline at end of file diff --git a/frontend/src/components/partials/HomeView.tsx b/frontend/src/components/partials/HomeView.tsx deleted file mode 100644 index 740a32e..0000000 --- a/frontend/src/components/partials/HomeView.tsx +++ /dev/null @@ -1,38 +0,0 @@ -export const HomeView = () => { - return ( -
- - - - - - - - - - - - - - - - - - - - - -
- ); -}; diff --git a/frontend/src/generated.d.ts b/frontend/src/generated.d.ts deleted file mode 100644 index 9205a38..0000000 --- a/frontend/src/generated.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* Do not change, this code is generated from Golang structs */ - - -export interface IndexRouteProps { - User: string; -} \ No newline at end of file diff --git a/frontend/src/input.css b/frontend/src/input.css deleted file mode 100644 index efe7f75..0000000 --- a/frontend/src/input.css +++ /dev/null @@ -1,7 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - @apply bg-gray-100; -} \ No newline at end of file diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx deleted file mode 100644 index 563446f..0000000 --- a/frontend/src/pages/Home.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Suspense } from 'react'; -import App from '../components/App'; -import { HomeView } from '../components/partials/HomeView'; - -function Home() { - return ( - Loading}> - - - - - ); -} - -export default Home; diff --git a/frontend/src/store.ts b/frontend/src/store.ts deleted file mode 100644 index 219fd8d..0000000 --- a/frontend/src/store.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; - -interface Store { - bears: number; - increase: (by: number) => void; -} - -export const useStore = create()( - devtools( - persist( - (set) => ({ - bears: 0, - increase: (by) => set((state) => ({ bears: state.bears + by })), - }), - { name: 'bearStore' } - ) - ) -); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index dba692c..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["./src"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 0bad90e..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import tailwindcss from '@tailwindcss/vite'; -import { resolve } from 'node:path'; - -export default defineConfig({ - plugins: [react(), tailwindcss()], - build: { - rollupOptions: { - input: { - index: resolve(__dirname, 'react-src/index/main.jsx'), - about: resolve(__dirname, 'react-src/about/main.jsx'), - }, - output: { - entryFileNames: 'js/[name].js', - }, - }, - outDir: 'public', - emptyOutDir: true, - }, -}); diff --git a/server/api/routes.go b/server/api/routes.go deleted file mode 100644 index 20e8f71..0000000 --- a/server/api/routes.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "log" - - "anexis/server/controllers" - "anexis/server/middleware" - "anexis/server/routes" - "github.com/gin-gonic/gin" -) - -func SetupRouter(router *gin.Engine) { - router.GET("/ping", controllers.PingRoute) - router.POST("/upload", controllers.UploadFile) - router.GET("/file/:id", controllers.DownloadFile) - router.GET("/staticFile/:file", routes.Static) - // router.GET("/", routes.Index(engine)) - // Autres routes - auth := router.Group("/auth") - auth.Use(middleware.ValidateJWT) - { - auth.GET("/user", func(ctx *gin.Context) { - user, _ := ctx.GetQuery("username") - log.Default().Print(user) - }) - } -} diff --git a/server/atlas.hcl b/server/atlas.hcl deleted file mode 100644 index 0a4cc2b..0000000 --- a/server/atlas.hcl +++ /dev/null @@ -1,21 +0,0 @@ -data "external_schema" "gorm" { - program = [ - "go", - "run", - "-mod=mod", - "./cmd/loader", - ] -} - -env "gorm" { - src = data.external_schema.gorm.url - dev = "postgresql://doni:DoniLite13@localhost:5432/anexis" - migration { - dir = "file://migrations" - } - format { - migrate { - diff = "{{ sql . \" \" }}" - } - } -} \ No newline at end of file diff --git a/server/controllers/auth_controller.go b/server/controllers/auth_controller.go deleted file mode 100644 index 09f15c9..0000000 --- a/server/controllers/auth_controller.go +++ /dev/null @@ -1,64 +0,0 @@ -package controllers - -import ( - "net/http" - "time" - - "anexis/server/db" - "anexis/server/models" - "github.com/dgrijalva/jwt-go" - "github.com/gin-gonic/gin" -) - -type Claims struct { - UserId int `json:"userId"` - jwt.StandardClaims -} - -var JwtKey = []byte("your_secret_key") // Remplace par une clé secrète sécurisée - -func GenerateToken(c *gin.Context) { - dbx, err := db.Setup() - if err != nil { - c.JSON( - http.StatusInternalServerError, - gin.H{"message": "Some features not initialize properly"}, - ) - } - - userID, ok := c.Get("userID") - - var user models.User - - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{"message": "User not authenticated"}) - - return - } - - result := dbx.First(&user, userID.(int)) - - if result.Error != nil { - c.JSON(http.StatusNotFound, gin.H{"message": "not found user"}) - - return - } - - claims := &Claims{ - UserId: userID.(int), - StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), // Token expirera dans 24 heures - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - tokenString, err := token.SignedString(JwtKey) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": "Could not create token"}) - - return - } - - c.JSON(http.StatusOK, gin.H{"token": tokenString}) -} diff --git a/server/controllers/file_controller.go b/server/controllers/file_controller.go deleted file mode 100644 index b7b062b..0000000 --- a/server/controllers/file_controller.go +++ /dev/null @@ -1,34 +0,0 @@ -package controllers - -import ( - "github.com/gin-gonic/gin" -) - -type File struct { - ID int `json:"id"` - FileName string `json:"file_name"` - UserID int `json:"user_id"` -} - -func UploadFile(c *gin.Context) { - // userID := c.GetInt("userID") // récupère le userID via JWT - - // ... Chiffrement et stockage du fichier - - // Stocker le fichier dans la base de données avec la référence utilisateur - // db.Create(&File{FileName: "encryptedFileName", UserID: userID}) -} - -func DownloadFile(c *gin.Context) { - // Code pour gérer le téléchargement de fichier - // userID := c.GetInt("userID") - // fileID := c.Param("fileID") - - // Vérifier si le fichier appartient à l'utilisateur - // var file File - // db.Where("id = ? AND user_id = ?", fileID, userID).First(&file) - // if file.ID == 0 { - // c.JSON(403, gin.H{"message": "Access denied"}) - // return - // } -} diff --git a/server/controllers/test_controller.go b/server/controllers/test_controller.go deleted file mode 100644 index 62b4eee..0000000 --- a/server/controllers/test_controller.go +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -import "github.com/gin-gonic/gin" - -func PingRoute(ctx *gin.Context) { - ctx.String(200, "pong") -} diff --git a/server/db/db.go b/server/db/db.go deleted file mode 100644 index 724c172..0000000 --- a/server/db/db.go +++ /dev/null @@ -1,38 +0,0 @@ -package db - -import ( - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -// Contient la base de données du projet 'cloudbeast' gérée par `gorm ORM` -func Setup() (*gorm.DB, error) { - dsn := "host=localhost user=doni password=DoniLite13 dbname=anexis port=5432 sslmode=disable" - - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return nil, err - } - - // Migrate the schema - // db.AutoMigrate(&models.Product{}, models.User{}) - - // Create - // db.Create(&models.Product{Code: "D42", Price: 100}) - - // Read - // var product models.Product - // db.First(&product, 1) // find product with integer primary key - // db.First(&product, "code = ?", "D42") // find product with code D42 - - // Update - update product's price to 200 - // db.Model(&product).Update("Price", 200) - // Update - update multiple fields - // db.Model(&product).Updates(models.Product{Price: 200, Code: "F42"}) // non-zero fields - // db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) - - // Delete - delete product - // db.Delete(&product, 1) - - return db, nil -} diff --git a/server/default.pgo b/server/default.pgo deleted file mode 100644 index 8e1947a1dcee585ccd42ddf7091cb3ad24d2c006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450 zcmV;z0X_a7iwFP!00004|8$N$i_}0IhrNA`Ih7O^xfHq}LYibIlVmda2A0+y$2xC0 z0?A~#xg2ZvZ){GmQ4qVU1@SryLALZ$SlHMcXl0{tl0EHon&$@(JoA5W{{HFfUmw3d z_uYxJt%UpT6Dk0WJnE z0Nikf=fR#c_5j{I+q%t`3s{5>z8>s36A#YLuM~V3I4BqI2DhE52k_$M_shFJY_<$< z1c5Uf{{4A5yd59T{?70wxkl!ex2r~5GL>C#j>%ZAda`S{>8zHWQI_0C$L$Ioc9H1o zMkOUV~njST^kg@GzNzB_lXxvLZ3+|C?9Q)#+ Date: Fri, 1 Aug 2025 10:16:30 +0000 Subject: [PATCH 7/8] chore: update the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aec1450..0f3e2cd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# projet Anexis +# Anexis - Bx -Ceci est le repository officiel du projet `Anexis` +A modern IAC for what you would like to build make your software reflect what you image. From ebff4197077aac5b6a0fc4d77ff972f43cc03992 Mon Sep 17 00:00:00 2001 From: DoniLite Date: Fri, 1 Aug 2025 10:17:59 +0000 Subject: [PATCH 8/8] chore: remove unused test assets --- .encryption-service/service.log | 5 ----- tests/input/gojo.jpeg | Bin 34602 -> 0 bytes tests/output/output | Bin 32951 -> 0 bytes tests/output/output2.jpeg | Bin 34602 -> 0 bytes 4 files changed, 5 deletions(-) delete mode 100644 .encryption-service/service.log delete mode 100644 tests/input/gojo.jpeg delete mode 100644 tests/output/output delete mode 100644 tests/output/output2.jpeg diff --git a/.encryption-service/service.log b/.encryption-service/service.log deleted file mode 100644 index d895355..0000000 --- a/.encryption-service/service.log +++ /dev/null @@ -1,5 +0,0 @@ -{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:21:09Z"} -{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:22:37Z"} -{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:39:22Z"} -{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-01-03T12:40:14Z"} -{"level":"info","msg":"Secure encryption service initialized successfully","time":"2025-04-13T12:25:59Z"} diff --git a/tests/input/gojo.jpeg b/tests/input/gojo.jpeg deleted file mode 100644 index acdd1840617fbf1ce68ae040d18e5920e277c0d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34602 zcmb5U1y~(T(my)526y*Bkl^kP!QI^>8hFEi@!Gk40&leX#ffe3Xp{y!0#=lGq(KmQNe8Rq{g@&8_kWMS!M4iUM8oHVYG#vwN0KzJPM zfAI8wc(Z@-f`52VcV~BqjLIM0RYOez!dpOidh5UOW`E<&on8OP$3SER-#dKx)7BsQ z(>qj4M@@Ce9T9Sp0B(R9AOnd1=|AK*gq-pLfcFjnVB-H(W|js3O`!llu>7wwnp^-t z4*`J2$$ypoYfPL?T}}UjgN0n7tsrxLT?_!PbO8YSI{+Z-|5XRM{9kONfK-t{>~ew} z)_?JUkpc0s=fDG6pg-5)v{F8agTl zJ`Mo^J`Nrp5h*P>5ivCh9v%e~1vTAkhBpj^4D^3Mpb!uckP(rwk&&_K ziSUT%|9{i(9sm;&dJ}pR28s%R#)N{wg!Wu?;QsjA{~;(Ch`9*Rh>%(W$h!Em zH2y|V&@iySmjP55C;%D*1_Lr($AN#V{{Lco_T?ZV(Ca%{q__u=QHIPJ%l}QsW+erb zF=I&xsW+=@z0Vb*wGv@YqaZV8Q9JJ?-)b_lZ9ITmE&IDTM>BIk1s_5K5d!r9)~xm%r$Lv zCGU?lMFFS@j`=5t`isiiL2It@KTiB#2tM@Np`MdUOojXUr=t1u1rK`QmHGg=%kql? z&haa4r-k#F8bH^`jhXZ{c|2XRwBG(y`Pm;F7-M$r#nt%#1<5INBT+ff*qIXz5O`D=u;qI&V1vuRdYjJ+2)Y! zWaNtIbHh-1Ml>;&i%7>+A!W&yfu~hbOQu?Rs`qW}-|_Vf10e;R9W(MAJerqx*^!b8 zNDDW|Gb|VGSv<_aDCvQaM_pt4)nN`6ygHZ+57h& z)XE^U#{bPIEe(Jk>ag=wJ^UNs6i+*0)j3`&wx`WAUjhJdy)Q;HGGXWDs`W0`qNK#4 zWG#i*jrEB)vr2H7`MsjO=Yj{T7(Lb}ddMV^DcU>CosXS}=jo?(o^TPJ3otg~@tOP$ z9+>_?q6ki&_tKrLjp(UZ%C;*^;4?Zq^64{fCEAhJL$G?FCP=)8S2$eF90ER3ER?tZX4VH;U zUp^h6f(@IBFSqtlTmjll3`nG5IGOi#IR${56IqsffzE#i0pQ`XXI`~PgMrV03IN3_ z7lOgHJ-OH1a;Xy?#sw9e{+9gHXYOd^za!A@HB}|GC3+s8)W_`_Xjt0h<7w@hGll zc1+j2z^=I>_#Mh`fZ%w<`$km^0FZLn<*~3~P3o4fz>lG|sqsjW7zt4Jt2;|pRSk{$ zoSSTzHAgK%3rrp=e?x~dGnUSYoiU%k(MKvi!?(eka+AQep9-+tcUPd7+oPgkTdzLT z+KU*~r2wEsC3~<&QmA>2#=rX7DJaCp1DIG;M)>O-aLuNXtFCA!{{qId+%X>18m)es zBubv2Wvb4Dmc5a9^>r>r?YOMISo_pwm_d^k@is_70i&+3s@Jq)i@J1>XmD@k%qyS& zEQh_EcsvZO#h&_DW=_99ZDK9R%Jh?ff60l_+Iy!vseM6E>&SD`R$F!a@;SJf?GY_v z&?4RJ;@5zqJwx8Z*v7KIGU~}>$v^EOIE|PGx3$(IZG>ecjqJR825HSV88|kUKY*`_ zq&^KD`9~rqj(`T*2v=TQf=4S(45sTH(tqh^j&V)s=xDYE#dTVS#6!{0NCpB_k~HFy zG9?lHdoCw6j`Kn&iqr$o>Pp_EuV>w&K|gJIGSr!jR099mG~^Cdm4WuP($ZtVA@cCE z3fDL}Zrzn#6ETZ0)5*L}Y62DzQbrR9al}6=SQtPw@d<;E_PhO$tbfCRu*f>SY+-#X z?=977W$eQQ=BvcaBa*BJjrB5fZ00<^p-NTSamxaKT-M~DilX&x4Mw0Dz8rfh8NKfQOeb9YP| zc`D5PH!w&Cr9%&)3Y-M}W*Z%pbh^A#XaDL8L<*?{19FZ0Wa(?>^wBBN!Sw=-Z!Y@# zf9M0yp)OsuAKu4s`d*b%02Ba_3h8bl5D3|ZDyR)SlaslA$@-Fg`^(f%&NpNI_rkUwnNZJ~;DWs_6@DGCvQRUJ$t zF$UEd1y8&^-YFukHkuUv*;#$4EBx)_ko~_S!z6szNFUFrJo3xQQ7$l0FLOI8+-mxW z-$c%x#^k9WE(yT&L!YzFQonWDF`Z~ABNZ^2c!>FhbRqD#b}KIInv+kj+huppL{>L> za&VxS%4*){jA*wxRym3c`%e*F=XGwA_c1-gkum4clOl&C3x2J}IjY$SOjNGl&W4|5 z3i=?=%Do|7HCplE-r?Uwps=C6i{I`0?#*CK>h2l7O>v6T znILW7{F?}bB~YX;byGNTGp!>LN~FyeTJ*+OBJ~|7dN_f$`I{u{-Ixy=XeLe~wPx@= zquaq+b!QA${NDs1EUfHqhR4FF%Z#6Co#03HaUZSZDo$)d+d?7Xw`N_C-~T~unKFmu2%`ZK*B!m29^JjOf9ea7V1NCb@hv+T z8O9ccL<$0~kg+cKFplzZL5@17kWuYe@R_TcZl3QHfQ`5P!A(T^#rM{m<1YF?hV3sr zsA5OzeU@<&dx0myz)6=od8~X>bu3tAo+G0B$3uZm_M?SD)Fxn%mM$Q* z%V5Qo;GFblGIt#$O8GAYmG5M}uQYrTa}XdlQC@#}7a2jdB-vx8o~(YtU#;X{w8EC7 z7*^c5XkhzGiEtm=Bv_;N1oYT;vA4lL5WS{pi7 zZhcoz=@ZWrZ$DmSf%Kmic*(sVE2PGcOSNxs^=!P$Gx3!1!#$R5HOK#VTR2e_bR?^* zg9IOb18E+HvL}AO0R$xhscPEN>@0MYN{x9}uaRUHmv7f^>Pp?XReq?s7oV%LS=y#p z?nxr$2$z{@iJN?4z{blE&RM$g%>B<6U{KP7LF#$MUs6{=5}4}DTrof5@sb(GKmQuZ zV}sTv4+tNXJK`u*D5H^-XJU3N+22jVK~AN~m50sEUw@}u}PI}OwqfUuK$Gr ziB?6R=DqF=qH>Hr>OfkTsw6HKT#cVa2B)4@p9Q(>DS4JrwQ;ALW zEm5f9VAj$QW|Rt{RJ5#~o&$v;2 z@3nNaZKJ?{$HtlT>qRKWsU<5aR5MBZbhPW^tzB++yE7;6A1V47ncJI@!SAX0UT=Ih zrOuUkzd65xK@&2=P}m63zImYHlpB0~X^^;p^yH0l((#w^7272u$7M}O8wik?n;ipy zf`x*Dhed#bq?G<7m|!6>Hyi+uiG@wUs)B(-$$?8n&Bo3tO2Z{)LM!eJiN+BjF*+0+ z^l#v9$tw2k&yN+W^qAbQl@?>!KzB-oN$fnofz7vT9fIiF64SNRqfrkY+OcPc{2`P5 zO}~LI+`Gww3@`uW%rez^>U_ zS!Xird8^Q^p4e9r{AjP}N83gG8|~e##u{V zW0;nj=vvp`RaY8r3f$*)!aSrCbFq}mlA=g<3=y{JOUc}5ze!?lURSCF+PLh~2^@FvQ%CG$E@+7K z{Dx5Vr`|k?8La%|@sh$dY~{f=gqR8k@chsg(mrb#2#EjI)hB6Z@eOQyF?AgFPCmN% z-OVvM@%Q^ZzHe4!h7xp-4Of>zyY=`WTw7U{pHXdCUEyRp)D$&dZ+D7+1KOqTdZ?rh zWCko-2;(JK(pP@?bFV`0!XhM)f)kx?mwzRFZY+TtxJ}6BDLR!GeQ?;#{pJ7pW)Oz9 zAvGM4ebRna?5^E&)M;CiiHe?_SuUZYmZQhoQ9^$F=CRP+!bFW1x}6zSjItC{o;oG? z?g>i>TwiYK6Q@ZtxkON-BK=Lj`O|NJ+d9u6JZgHYig{Bhx`BOdCHcv_Q$M>)g9SN1 zMS7CaH2Qdn6jm{eMGt*pUsF>2Y;nxQl4vV5#A0*_KNtmzzAYmA%l&%r$C<=292Wkh zRe>870TUwcPMJIlj;VWu(j_hOo30&p=LPw%;cu%(l>I=F`v1#hGG<6=0E?_4nl~`!g??T# z_tO+=4Z=?}4STyr0lEe(rj&D;>MwOX2nC@njA7NG)O2$uq46?{shwzz!-G-o*%{nA z2nmMrtFikC=4gZu3k~ZWJpM$qD|&9cdDET#O%>ltO~&F7QY=w*X%y1=NJ1fwQ>&_9 z#Yx4BWCu6@!0YQYlY>!1gQ+idIt})5_6g%hVS_SG3$s9bTI#I1kD3s;wsi81ktZL) z5?%K2HVOv#-aFr6N>gYnY3Yn?=bbqgx7)=}$$72{C)=i1@?jMlydsu$%rJPVQbeel z(z(S97T4*pwA;OA$b+8Lc#H{IF`U*r2C=92F3nhZ2}x2*l**CI*1r^~&my9@CCHQ+ zDI$>e8N6vgiRLG;{=$j4lfgnQdrSorw1v7?;<_;{R_x!c%@s%&KLk^VrQdh|nvire zH4Y)2{k+tdLa56bFenHLBgjsa@sOCxi=C1lMankV>FM4Y-m}0&$3CMw=10s))QURI zw6REU1P6Iwhnfa!B;I$HGW+Jmb0k{-2JmtN9*y=~_X}Z{ za#n=#C`lCtfe5DV8BOLr7E)#?4jB6TS&JlWIpWA@!237aRUc!-DYg@= zX;f?JpD8b!+07Da2Pbsc5+W3~%;iGI)%2s(wuY{!>f}T34w8c5B=9KGu&F)Y2n|O_ zMn-7STvbsgtRcMNLT&JytSXWAaH}vSrngK8apSk*AyeDQKG+#t+{Xm3BWDBz%^bhE zBW7FyF)U8&G{?$#rMpu${qnem#u4YK{p=32#jPHnuO<642ys&xpN}|>mhD>TXSKc2 zMhUkJwT06q^f$B72aIqI2MV`F4MaqJEAnin%7xtBipUaE~m@_hWD%E4gG!%=|8 zFyf$ylB0;bt8s^U!jb!-e&CqUB|S{={X;aU70DGTKkFNNIBFVsCNZJo;Wf-a&a)LA z0q(tQ^F>5LyhZ~%Jym$NZla*?+)31`yX$^W5^o`o;I3`L&@gtkT6i>V3sM?OUYv*i z_FCdn8Vl2P6=P;g>t0{vLvS00pV&$yHXBjo_oWQb1U8$AmACuk+h~|8GanO6d77@2 zUYWgk9rBnkS(k7^G6@@jIb2+k-1se8L$$Gwx{4E|6?|9JW<%%c&7*9FG3l=##-zud zZ)xxdIX-cE4QF*GVWJ`lU7ikCNBYa_Vr_Fx^pP$G7dh)@a$6k-h%tbloQqz)?vt5} zl*qw~7N&~McGWiHJ=jLEIDN0`I4~m<6pT3fs}>zK^$Bbk_tnvlb*Ro`u4JAZ?Dy;z z;i$}Z#mX)?MgnPtQ0U=mH$5VBxDqu(*t$&gZXh-_p+Tg%eJY_=%rEyLSTwC01nKAU zWLp@VtemihV&s;kb9bgxp4-9LsRNn;WE_aG!T1k{ar0*>c)j6?!ab94XqZWTp=)H^ z{JNBA+@}69r=h++)OXLSjS`Vb6W$N3tli`0hj7Ygrtj{2DTU`Fe)&&45|YWfT2#?F zMA+nw?-%iVWn_Dh?2MdBFN#p_KysXIf>~;(9It;o|3iM1 z)JoZ$V^m}tl>z>5-@jZ~yyd(z&Ph9u@G>q5lBozV8FeY)ak{wm1@MOB+`enSpkRP%N zw|5D_P#=0ehfQOloK8N(K>6p9{kY^CTD^I-94mG5O{Ynpw~9(VM$+|ukVxpo zIECeD$b+z-vWmWkzUeNjfBL{XBggID$&h^X-O1Isn#R@s?NjHz)%Q`cgk zWSP!{icrj$H&K=&li2I6OPL#H>%sLfh^5WgmT1$=SQNdJ_4B3(k&2{V0a0?qj=7-3 zx@83|d{zrBT3ii647U!eG`@-uG>h9Qk61#B2I(6wvlL(7xd%PA;XAl;VU}{LP7(%= zD@i?~zEx)D>Ew}hbR28x$>7;@bSa$ROzZdeo77>L$e@@}?mi5PrXoTN_;e{j$6tZJ zmG18C`ig_dWX?2hyv(pZbhj~Wa5<14k=$lp?Of~dft#aW6_@GL^_-7XKyOq3nEBP+ zRwt<}Omcn0*$_ey`gzCW#lpa04uf$a^bXn+Jl-& zeQ!Q1zt&&=&?cN(Ehn`%qqMnxuj1cWaY<(tD+HX^HlJxjJ#KP#8!M-1Ui+kNe6g4j zQQPR%{Bl$bb*=fUS~Zvkd!C$d=i095Tu+QjfybO%3DHNWlZ8aXG)|o3j9I{}XtusO zCAaLxoFARkY%)VZv9J8pCsB!1DLKxgsOQ$@nvU4V1X>zntzjtk9Z}24>x!C)hjuOt z-?=A#<FAEX5R5Eub(2Zn*g1aAr2M9 zryU-z=<#@W7QYX1aPLl1Hz&Njy$j}0tezYVU}rUCAltJRKcC7S7#LB-(L}9^?l>m0 z;ENxn57AhxdGOKh@oqi#et^GTjQ@(SkC0q_&fGS&rTJi@!9pE*X`rKkW?Xc+ zYswi@(RtHsrBF#7tfExCRk7?cC6Y5|8THR{Fx-%Bh(-Gk?o*`Fe)?$0Tk${#`|?j; zG+?2(s*T{1@g83Mo z!sA9H_7z26XiJZkBM*;$WF4+*Nu1P4d?!l+G01v#jhZlVIT%l_lyNEXcB2n;y*gNtX6C)x-LrFL{Z3C$hAs|&(4jRM~Mb-oF{AE9fEJGDM6|Dxf6u4~Z= zC+x)$%*CIhz9C@4G2`^rdC3!T=_LDP?DFed;R!fh(2@De zhdC}14baN?QPMr}*>~#I@ReiibV)E3ew!VJ3e1zm>lyq4p4J@4f&`K{kbZKLR8T{T zicbF;m-A1%X2XUK1`()E;qZ0GcCZEeFzjbbc}q_>Dw^z9bAj23zjA-@V?2#clto7K zn~m%ZfG3RPvRao-NKG6Z99(`V>WRS5MYpdfeSCcou*q{UbU^K5t>9ke!ObIpg=(+b zKH4ZpUq0Q=a#BgLj`87W_(()>eQ_PRB=u+pPo@p)H46>FJQqLOoy`od8Dj(MjXBv8 z@fr)ljb_c0+%)y-0rg6#2bWju1R9N^!qTiS=HlU8^Q%u3t!?VPbW)6T)~@XhmhP`K zb$z7F1IWw*2t?4g++~{&S5qn7A}+a<5N-CFn%34NVXRWbts*lz*aPtVNveGH>RLz= zWI;}ZEd!1&Nxb6|9BwhIJ57HWj;M*G)Yw7sQjlBfW&>uyW_qTavuiG;fZCJgdj49U zjQl6Hx~xSt{S`6Z6_opMUM$ULqWsALPP&aNBtovXaEKi0}Go?l!B6qRZInzT{STG zuS66K6qE?mZ{XE2or=d!*?uVEwjQ8tipxF{G#G=XLv;1y1W^GeW-6US+LkbW)C?n) zA%N|fNvaW-XS<{&C8jCOah^lkW*i$18;%nC(_uY5>JU7aM-aA+@0G0+NU4vER5|8N zPAq$)y4S1Aa-3D?oO_Hx9zl1%8sd=*d6~hGvpvF5SIv>J?vR2*8l*A`inBw38wRqM zKrTIIF2jPEN1azsWnAH$#8Nz@n&>eIR8e~s%+eps|Xxg-8p@nmN}Yp zY77}Ly1xbt9vU(jQ2!V(016Wt3xk4^O;iP&RZP_c#+3c9;e!4%T)!mrtX4`MN5W?+ zF9W<8o+rLcN+_Yw&F+7dQ_l`H|Hk#p%E$IFR&D5xVYS+?T#9cqbtR>CszvLi?@_FcMD4{$ED%5hR+rf=^UL6@OSQu?y^amf+z z%!-!aGGIlLtJAb&C907cgUd>=?fnJH;Pm|DxMx4eDXm&nk+P~b+Oo(bkVy=Aqf@a+ zZXc6c3MBLOfSMb4bzVdJ{8di5YtgAV*0Ljw0y0{hYC?+Us3+LblEP^W&04l?ZKe}vswJkI=iA9BSb|eH1||u2a9PD*sm0}Ug!}ZOWhr9Q z5vdy=kNtfvGRsRp%8xu9Bs>*-4H8n3DTmCVBqR?D0|kwY0Qq_g7BZ6%rw5>6FkvaN zs94$9#Z**HT`;gIsKwPjafq6^CgkBHc2B`^2IYfkB;2-4E^*bJll~_&3or5~Gdq75 z`>KbR`Zs`X|6yoz&O=3AoI^BB6Xa5m;}&p9kSa)x#I~a|+|nz%I=d9xG$rj>Z>b|4 z;`j=Zp?z=RMG)h&l7ul}N8~KS6}WMKs4h%g8|UBNo1WrUdR9CK(=@zVe$mJxV%;$@CRjV&UV2W*6DjLnSi(g!9{F_RBK11rp?$ zqEK#|k7+#q!?iNiXG@-aPj5Uz%P7|b6e#wI>hDF+2|EV@q!xzQ)(W~jEPn&>yO&?R zYzz&9sBbW8tUYyScLcANwc<4K2T29cV#T6V%`ajOws~?P$|py@O~^&0i)&46+m(!$ zN!e%X<7+&=GByQFP2i&9$TP*?0Oin@P$gy$Xvv?FV2v_ zH3kA|zkwm&4?J+e)}-c5$2uOxcujGqZABZM#*!X3w+9~R`S!$V`5E%x`AXIW(KS!! zn&eme{fXw>+@Sr0aMm}!dJ^+@Lht&s8NITVf z(iSm)1Lt3(-dwPq3jGGomIe?H>eu=^^HUvF+P2P2*^k0HPA(hUN}XS8!(lvp;jzfP zm;V04n!;m}cKo0i>HHOm)~l=z({;Bu6Q_7C%^z_^d;D*f64kd5g_d12Ily1zI8t3} za#+>symW+RFjuN#3OGJ{*vo$BAw$akw*4FUT+$&w7_aTzUO`Nwv6ERLU=aQ@IbzI- zZE~erHb?FA-H5<~~W$KX)mlQ=4!*OZSqEl&0tL(@u|fC?lI1O6%V%)ctf`uM+$QXh0(VDY`P(m*#SP&&lb-TckVBq`Q?> zr+X$#CT^2B3biPL0a9~18S}&2S&!kg(ICoc?+FhirhD*C1J@)lfAFf`bjd*Zl|A(I z>RYf*BJw#O(qONqwFq)A;o_z`nV@)!_ewRKbtB|hRWLb z_2%t`k&Yt52PYkF8{wv^yMBoL)dU>>uNBSjlFa)6OZm0 zek?+MVhyei&8=%2Izs4{XN>)~QPOJbHA4;|`F>}?xOXeK*ZU0RIxRL}x8ph>-FBN|~AcBN}i$i6x%47z= z=1S*fSx`H6(3{xsunNtZ3QQ-QXV%JbBIP2S_!j@b=RJvpb;?vdlejSv+f!IW>-E{iWijD7Nsq(JT`I*!k0;;O*4 zlz_bsPx@SdaN6sJ)rtO_{=pZ*gZ!e+h`K?20k@G4*`#I7Cd2zEt=@^k_`i~0hxizP zYnH~Cbkpv13Q6N23tYEfKi%q;xsO9aI=CnIRGr~PYtl{lwR=~GmXTZml}6JS@>TcfU=%Vc zK3w0-byGtQ;d1ylavb$~*sw*_3m3bXD%x8cKQjm0geBVET=e(q3ppkV8GQFsLbIJ0 znz|N|3i`@*HnydbW>NR-eZGrLX=Qcm4?hyH=f9ajA`T)&OVl<2m*~A0Z5-RM#Cv(o z2qH$$zxNW1ISbJ3Gxjguwyg(j0)X(Ts zE;`Zso&MNg3SsKm&^)+L5@z-MjN*Gsz$w?K|NDmEoZB)q!yI55|!z5L^i(xhRMZ&{&<^q zYn02zNe52D(k-KEXWio}TWz2&v`{M}CE=-(sy=F(QMQu$PS4Udxkxo(q2H?6O`(>% zL!IsGPB%a(iq}wBAJiPv!TI$%gs!2vky+wiETIXm!0d&8;?2g7R4&j_bZ3#1eRn?T2M{q~?H92CLeO56;FT-BY52Eb3 z2}U9B3aejuK8z83&;~Z}4YQE#1#FTB`DyElkzm@T2cKhRwn`2r>B{jYLU>coAsBjq zOUIe4@;6{s^O^wtnJnX1&!i>!dFzXOFl!rwThsthevI2kmVG(5oX(NF8)7&28?eXF zTwe9y767*w@OMXJ7JGaMWubVkI{VfUV=fiJIBw)iTMdMMunhN%EJq3}SbfhOhx&0v zXyC>ZcTOiwJLiuzrYtUV&$;taP$drU<_OmXg1TdK9nC7vi=nbS-^0Y2U4tF9cYQ|r zx{{0z+;p%|k^G8pM@Q+lTq8(%%yXQ(2`*M~tw=ei@Y0I(x0+ zTGAePe3e}$;`QK3Znm_!8j9I6S+2G!4e)D7M-a*ty8G}L)H;Q~udPVFuPs!(K<_MN zSn@_s@33GY2;(OoK>*pu@O9a#Zna55m+b~{XR?;<8-Luj0mjZc?Kj+8=Quo}YntD4 z5Waj-ys){`nW;POsPu;OAiULy=e=RX!-I3N_lEPk&>p!tu4}PkWYeRydb7P*l`i&6 z{zS4f&DYgCG(aQZtGO8AozA@{nv!;{>S6slUxTUmeMh?hZg;oG{c2le>%gN?seN5b zC$_dHqkZ$*MSY6sOpDqi_O4)xIkA6K9e+FY`QrTjk2crxR?byCCdhXspolY8;&Gnewir`SmbE9RR*b4mYbVw* z^-|~KMa|czB+k~WBvMOMIEm|=-|)DcnM*Tv2Nc!IPQYKb4eg{lz3*33pf_$6lTxu0 z+9f28;R*uN#2sx+b1I}%X~c33*7Z0tOd9(E0q3Q z-JDxJbZyDINNd-=I`aInfvW2{rFR|a+D~PDq>mlXWZdLZx%xvDa+!Rgt7F22Z%AIF znN(ZeLHQy-z1{0u#r{v_d#py7+3&m%PR`O`dDGs@il8E^;P+HL4%A3XW>_ zz0oYLFgJ|l2t76bS-v>e-UC~+c%PU%`GoeG%d@y7cmzzW?H%jiSG77Fte;t78sb${@3dHuUjMV#a_YSN zC#by){kA5+MH2?;#oD8ioYHvOhwl~0_AzW|&`p1=YA;OU7CG_VT-_-CIhY7)qtZzH zn{r>ud+g(hsWR>=JiMeLR-_@8cmwU_ConiNbfaF0B1GcV_SNcYKg~Fdv$Np ztYB)1a9l0P@7^B#DcRTd9hjb<-61d}bLH{N?;DKFoVy?8t7?77C(T0bb}?g-6?qol z-m!tFe)3?1)Cls0+wk-0hYTWPGKpLTR(z5;h{sW$!t#Z<2#W0?7L_eaebsXotD1#$a7v-bt`)$^Z$xU7tA;9dDwQS zZ@9hP`V6zWxpF>@{91v*E)FM$RrR$mXwP1Ez?i>vmuTP%?=jMShD6(P6sw(Q-wg&9l!D`4?EwdnVV^ z5PQ8&B%QpppYe`zrr_&UPy&y#+eJp|As?d^jbt01wKp(R|Y>f{EP{QEC^RTr}BQtMK|` zdc9MNR?1Ar{WL*67Oz{K7`GOMJ7?HC94<1k+$ zJEX`Q_+-0PV|dU@i8l_C*Kj5jjA-WieT?yFSnV08lX}z!7r(~++2NhomYO;tu~8AT z77X#E&OR6YasKIF1a)ppTBp>r|c7}QwRHbQK_@# zEvMy-Kd^A#I$Cpj4W= z<-t{^gz)pWcBJ=8Y!;E>P3s#(QDfAe8}sH6&JN&pv!}1+FM7n zOi3bdddSU+c;rLblrMpE9CnGrGRf#wBf9Cf)0da{M8T((Hn3FyG{$=;(4qjA*a2nf zB#)`!XMKc^uTh6IW>F7%x-w9ywyOOnD58Qubq|(4ojAUoN2crtAUek*^Ph2AsZD`r zk=EfE@lo(251kDN`i0e9lWT~ZCg$*Y^#O!GneN=8MLrv(O1waR&CXF@)5x7Y_#95Ymhs1bf} zq-Q;nbpFUbk+2W4DOFB;T-DawS{FGLAV8ZYVp+|ytbg`uIw3TWSYzLRIl6C(-iVse zH=Q6-CE*IXk=^STDbkVV7&-Q#-&xmOOJz=YD(Yxu!6oFd2;9kH=Wq2zj`>H2A}8V>u}&fG3O&R9l>S+n>ikCLQGeOxx*3PaJ)f748uj%rv6oqbE@SGE zuLIV$ZMQ3i78-ad`@)XM`QVYB_F53#9@FGLZmu(F9$wSugU5}8uGnXZI`p@=(pz$j zlXIrRvGoFSE6uN37DR#ixCit47jNW) z)x`pzYJH6845 zoo&Hbk+YtKuV(H{pnK8~Q!~|=@N2Z6WQ?BeA?bW$^mWK`&u<`@X9|m%#m%o?Pt0i7 z!dIc1t|5uSJSw){@%lz@gDL!2Tb&yXm(eu}tsQI1AMGw;$@oa>C3`}oqWmjnTp-K6 zH|h8qKdnu241FbstI6`+_e`74AJTkc79eqA+kytU=jRmc2%B{+uA;YNe{G&j7Ss2e z58N8wWQ?GjGCQ|pv<|Nu{W{3s)Y^;LOE24x?cQ6|D09GTKf ziPvs1lAkJT`D7qqF(*SX@k5mhG&UvHh{zX}g6)A)l8>2*u=%Xa-WRXE#f2s=mowMN zX7wTJPbXKGIt5bRYIQer1+dCcKo8c-2b3CzXzh9H%|rH85xU7g7-Nkx7IdVU#J{zv zla7;&3^qs|+Ew{(KsJcn*5qlkIw@Y^?DCwx#Mcdc*Ssy~?TIA@wvz>HhwT+7+;SzB zk1X{EpBH9q@D?HrZ{|V;GbA+0 z?MEgwalmnhqFFA8ip&T+YxzELpq5Hdt#=>pBSDn&{9rq?H33fa!x$$VYSobtSLRVS zX*HI}hP&l~B`YT{&aQ=iml0T3i8r%m?w{&f<|r4m?Y9OPU>C0Wv@MkMLY)wSm)f_~ z^Eh;|L<)QZbG?My9$0%x^t+IUNu{`Krp{GrD9KD?oQS`Gnrv3ucH~s2fD+P=m62w5 zjeD=pivxij2mce!bC-IJ)A!to?*4LxBQwkxX=XZMZ!ZtGJM)gWy|3yxu$N$+7*u)6 z7aA*eH`015ZRxFz`Uxqq9(tc>hhjzL*toMNICXPB%iL}fubn8?#$rH*jd7%!ky|@X z%HbL>j-`;;!AawSwVmQ2d!haW!S=iAOq*0^8G@nfdx=J&1>{M_R4KTk8Muh~g+UTP z(2uW)pkO{_6VKlC<2fIt0q0Cy2SnIH#57 zS*4jCJW$>1H3x%l<+b}#5nxX@XJ9dCa^HU@5nY^uL83Hmf-U7|eN(EjE4eybZ9`Gp zOx-EMM>gE^*+WdB`;&YMi!-=nquI(~3JQbZM!8YFr}u2C+@*O_br0hCyo>pygaPj^rbbmQ%t&JOhNJ=spcDgp zUTD0P3!+S@Ks~%Ogf|3~FHp^@RU_{}Wu2zJvt-NY)2^3H?c?WOhF6V(C~;I+QW#jumnNe{2|XFFThC` zvv5SR#6Vi;3`Wo%K}1WwP9OIg2|6hbw8azw=XmujkFwo#$P!}7wq9;z4AMPCOb>kK zkjthKMp`o*beD$kDLq`)CshR)XPwPYx)O9OGIha-I-8=ZQ`}VVB|fu7EmF6M2hsj; zqkv-9Ea6Ac%Y|l&@0I^7p{CZBi+h5M-qIM8C=J>p@;PHt+*FBag;fb|T_th+)7W)v^~aH-d~| z)8;==uPxD3H4Uwm}xcvt~7MUSqWO&l*7 zj&Gdbl+*|u;9-)W05ICDI1~v%aNk5Bx?ZU{TyYMtNytHi7`qztV4WaIU)Ae_)dEY* zOV>~Y^sLL7u0YB{9`C@6+vi8C2{IL>kK4pd*L2dQ4n zTF;uMguNJl1uOVrEGk(f2;V9mNFyrvP=Du6`3>Nrpz8e$(j9B?DZNt@64zadH4lYX zw4y~DTD1J`qq_5@%;9<2;%pc_?`{t04*7=4fYlTjzv}H|au1E0FeVgKiplGjpYq{F zj(BiLmad?jP29Fx6=R>IDpL{`$x1sc@bax^(3@+36f=?8;cAJwJBq@MQHZ+_t0q241=RO`sO0BR z9IC)wM_KIPy521=qLR>@_9^~ID#b5-d6MkbmUlYWZ5#;p{RSS&?a!?rsgC=pe3VI` zkfjmlgmzJJhGL-K-(7wx4UYH3GkSoIO8t^vzo|7)^K2>_TavRNNjVJPMd(8D9dobW zBXo8#ubA(G)5+n9ZyLo^0(*m#Epj$oX9z=SC>$ywh?*a}vkSJ4m%K`F3d5W@yb%&$E!nueRBkKiPR|6_xu4+i1 zCDEuu;g{YhfcG%!O`;H~vV>LnoT0vp=MK^HzCvtF4()#x#fm{vI@2F|`iYoDL9T)p zdireldeKY;*iEOe)8VRtZtG>0{N@mfGf4RVH1-x?ZA9zZXdn`>!_2?C{bfKj**p%@EcfC`BAp-lZ&*w0f6(Dy3X zx|hTit(wJxfx8(sAvfXzHIr~I(0P9nxlT1rfvsGVmg<9@hadpgOQEBaaN7ej4?{t2 zSyd(&;dQp4+u`hf`#dUGVT?nI);!Nou!%?jIWG*TnUDNMJT8w!5)J{fnGYwdvQr@H z^VnC}`))Y#L%*J>>4o&W91{344Fd`@4iq!zH)^W0bW?)Ro_J|veLnk zRv)60!B#7;-8ikee)CxQycP=g{N|y&$K>Q=b#?qJ{-0VP6n|XF7E*{vda!m19ll(w zy%jDx%So9j4TTm4qvZ2=N6YlTOGCJ;SBgISDIK}*q`H0VpaRCn0gX$|?gT?pieAv%f#C-o)r0Me5b7MzIvIVGhy z5>=`wUMu#GK{9fzKm^E!uCqP>lKe^O11Lh^HX?>|LdLY7_~yF`nhk_8)pvyIPzXsp z0u2CvbpzC2@e($G;H`*J0S(7x*<47V{Xf3y2K=3G*Jj(dC2)*&4yDV9Nyu2+8 z25s>OWrM(g7gmvMc?@UL7&QmhQj)F@3o+zkM6ok#Tg8>Dy`Vi6zff6R8fBvxl|=XkQhi5` zOmx-X!d#Z88dRN}ImZ{Xd2+4j_aSMTjucxh0Cp=HltD8J=1A9sMNNm3_!$cq;{a;% zPI-^x5D)ifbKmhd=$ze_}9;af8;eU zGKoGZNq zB82BL*-Hw_laznQB+W%q77t9Pw3LViX1mEDz*GAlsxgSeDo{Yv2OLzN=Y-&&#NJI1 zc372n_oLjQzI~NHWL0OpyxJ%s1pkRg4NH+?XL!z<@tTX+dVpfGSAMaE<)Z@(?1 zXkb!tmzl3aRX;ut_Tb-ZmS!_m=3SS-cK%60l7(*px1>Y7S-ac7jkaNs*hUK2;$Frm|MH^M%f4 zwn!V49Z3Krad8lL>Qpmpk`xf)29n}3HT@a;J3?puo>3;vc%p`_XCiRWPNhYCUbCR# zx~kPHJA3d;M$Y;{dWd}@^jR9n_h5h8k=kYavInA|8?1i+CHlpD4ROvup26P!lmCPO zYGJr}TbmpYE>?HA(ZZKo8@*s`b3{^7a~8`(w}-LeckoW#7G`y|4Bzke(Fb$Oi9<9b zmpYZ`zR}1oC`F^f;n$ucwU;T6_)jr(5MTi%(iT$CcS-g^2e@1FLj;V~XvGgIZ;4qp z!%;EaKByCac^_gLD+$JyF@z&&_yfS=m*njqVl=|N)ZvJUptE!-@RIP>W+c*BNanU^ zwEKd*D}75P6tvpC0z%HhvKq)27Ufb~1Jk1hVO9mk{D$y-?gnq8s-(N2f)?UlJJ+hG z`A&0d19nShIg^~r+maJe{U6*(IXNFEkpC8*iNn>>ze7m~kfHEJ<2*>_4;LIT>L@Wd zAm4rzwAk|#l<8P)K8Hy33{A3Z= zTNLEQ8!}VW-My;KyGgwFxXTRuRDCGJ3H5d|7PvgS#jA{YV`X;g;pTz4)&(^FNN?$i zejyJSgBAP)ARja>=5fY29(C|+ArR*@5;~3*AmgD8Ta;d*GOJU+AdR*W0kQxE!YTtY zREdM?oxBV4Gn0r7KBc!Lt+*X#5rUQ(Q$;c7*n%6x;t!yVl9Y6(?H3{5iSrj+7L@3i zK%7}x90WPy4R7qpTHKYu*Dcb_GHj+Pbfq<;P59`zw%RVq@GViVf$t=Z9j z_xHDl0Ngazq^FwcOmc{mRunz9oe2H_Ako}kOGhHm;Md^oODUE<_@B2Wu`Q4nYYihz z&vyF)sYMb~?rV+ImDu4H2ALsNo<39WqD@e6R4RCc3hc1`1;u7SZ1QNbLr&PgrFd|h zDHE1%(#LE(Na_WTuWG7y9c=8Qi!fk)J3AT)AojovI4d?w02@PKl&J@?5O9!xV)dp;Ff1tbk(#&Pp+{5ZYM8 z)&SXg1dvI801(OSzW!O*HG+o>U?g`zbjkOwJI8Mp-=<^fzR2|&_zN{QpMyWQabie3 z7A@EX!Y0_Sc84**m_ZcDyFTfTM%VoAB|f&#X(!M_^Fjz7GK`jy&4$7$6hcCPPrgJL zuiTPZ(jy9tOFt1zFBBDp!zR@q1x4hYKw{tH?# zCvC&2DBI}79+#(}=$#TiI5K^I76wQ(*N%+_nk4VRw&*xmo4u}&4nql$wR(Ij+3c!B z=XH00o(=^U`Q()Z9KUKAq_zeph{@*_lQY41^c)bN2lj4?gZwk{eG3Bge9aYn=r=@9 zKwg$K7K!J70G~dBk0`WBf3965u&N)lahem2u6nMhj1=P4n>2`Qk8JI5rJ|IFLsLrS zc+GJjn}_aCKoR+o8`Litg!DW>Txz<;ljdiMx|!K|HIQQu@3!yP-NLa-C%Fq|nNSd? zbcGUvx#|aTk(bjAao6}V`%H6f7hlVR&Z8O!rRilcq=pGUh{G$G(rH^m>shJ`52Vit ztwJATk+bek5~L{DWG8&c!FuE~gv_Raf`K^2{=3^8QXz5TlqR%DKj6MP&fp+_q3E?> zE@=b$p;u()QSZEQ4v0%rrcUKPFgv)(hof z|MFabv>N$(x*OyfSjDF)S`mN%Plbu#SkHkx7LA**dxqSK*uQ^TreN>39Xld#n5Zph zwb_U3^dUfW_-I-cI+pQ(d)|OY=|usEhZ97_{Xzwqv{Pzu4M?M#3r*8@52O$#4=*Au z;h@T_N?Ss9@`Mu`noufmV5XQl(!%)p8A~7TP-ZRXiQV;S`Ywp_I;hS9h}Idb&MF_# zpv^>qDxPNIN=b|YDtgU0;sSAsr;s;TTT135<^)HQr7ATnD1dFZka0a3Q(=VIMl zNVp`aIJVEL@F2_HU3C(r@+Yj}$*^RZ^B_u(fc-7-ECbo81rDEbv2;dFwdZUZ?~2n?kpEK~p!aJk@?l_+ zBA5;W_+qXdA$|?i5zpI~0n8Ku&|j~ZNlGu3-{p%%ipQ!qtBLzwf#wY0_9MhUPJN>5 z2EBtn3b#3f!*(G|mH1hNij?;%Umd<288vucJ?W6*l|caq0Gf~Wt+`0tXB~Kkk4%b; zEkUXXI$<~*LokJx2L^LP}yYN#muYInuhrA$HKwkxMF+kWOO(E+ywhzxwp`wA&9K%IhbnD*^k5B zjz_=|+WHNr9j%hU#l@j!?jjS@n{8cV@%;ga6PE55@k?nyk@P4Vok&l4G08{O~*7Bw&8(ue~GQc@#ao$oqSv<{=83 z&Y)PETon3_JrLh{{LKmt|0aP@t$LUyYTk4-d{BnJ zO2`Fi8wO_dCNTj57GLNv59G&$T^~NG4B`mk*Dr}cH>iLdfF=?C74&%2c06--F6Y`4dWIzy}U4u z9?ptGrjwc4U@N$KVlp7X;~hXf3&+JUubj=AB1Z!gMO-1JN#dG6QM({fWpZu}@G;k| z+wf@XB`FG3(exU6iMRQ(r79??kVzS0d|y(`EszQ2uNi%x@8!5BTB#EWm-D84Qp+l9W5|79mRl zzy%}W5^d&zNPtUE@yc-HN}v#JM0sfnc{aLO@ee1cR=;Tq?CX0m@)2^2-jImFip&2Y z!ub!05XjY}@c-Xs^e^)G{}bQ)x3%-H!I=LSvsFA-lPun6uvVHM|Jf9SzN7F}Zvm{; z6!zsmVnRf(Ch=$Xg)gNt{u&v@{a-LaZ#m)3eqnAkf0u6^SCjlv|I5y0q=VqkXjl{H zB>Uq8o2$yd`ci|fif-jTvzrKCjgVZPv@sumwEo2i_^<1#M6@&ZtqI|LHGbDY;R|-* zo&Sj$uZJ1I65fdVrfjN)`+8mYYJ%&M78cL{_0=+%H> znBb{BZp(wpAeznPkBXumiRIjvRLXVAl)3&l=6s?FI@ab8$L4fz8Gtv;ch>2-@Fw6x z)A`xIL?U~Wali2BJvxcP-uZaeCRq4C!+;_ATKUP%(FRR+V!h8`0N7Ha^?$nJpT6n_ zMLErp`;GtE`uF|*`su&J_^&U$!WXD-{_*q9$2b_KOYL8c{DqBTBpY*$e)zKT3*oPk zr5&Ar!%VPUuqtJdI|P%y*g7^oo;AzHVf~wk(>E-zjfAXzA2lYg^P^9OLhat1p%{wt@O+hVwXi)Eztkwo;r68hUK zU2k&#J?Xk)MBYDu&MmdSxGT)Yhl`AV^8;IDdE@tZCi-~ad*&GZIN4tFZ*Kmv8uSkb zH+iWrI}gqOn$AD*(A>XGq-r;?h-<=K|C5mhI2EMHkjVq9$Fp*y{{(;?4m?bE4=rKz zXI)n@{4PCN|2x{NNe&X@_iby=&XI3!?;6MdlbtQ44d_+k4*cCj;()!;zIF zw-wlM7H6wW@x06B2mAwY&bj&zW-j{xZt?eDH#B%IMUpoRHU77iNq&P}i4vQoP=_Na z&$&m0j{o2(BBjsI>{q6Z(_j$#Mn`>3?Y}2IMCbhnFvuW`!FYWzwi=Az^moz#_JYIuCsz$dRWoxob@I=z`v;flFS!r$2Y_Z`_`t7Bu3ax$ zvE}q>OHgOq7l96}Z>^)WWzv%h=lY{3m1tjY!je7RW#3@4_*@bv%xTg56WjZg%N>e` zcC=^l;O}}rGQ&!VVz8>6ad>s%sm= zc0s*70#zfS@!Fe3LA?T@wYOO_;rH|(lhyTi;)^NL(|2CV*Z~I#N=;ri~C&@WAmdN z>)JgG8C^sp>Qj+|#*tM6wwynSxYf{Cb+)}poPQJeb6qBVi(LAWB?@ImILgN~RRviR z5_~mD0NYs9STGtI++RZ4|DhJbZkcn!ww5@IhWU46CHyz95VoZN1Q)pIeL_K-82TUO zlLxvTRP8?sJD(F4TsfV*BE?un^TBw_uB-?v@~%#;-`K4i5Jg(2wT(ONcyGg6!1O~> z$ll7t<|ohU4MdN+xtTHD3u~fqw6BPJ(tgJvaBOmG?0JMRvh~ywJuoZ5p&6?5@XLzI zo*R69Z++yGYKOd> zyyuY~T(9J$zc050{%t0z_nIYw@u;yz8bSQ#<`}bc$@Rt3oFFP~<5<)B3w8oQ3O4#B zcysMO%aNe3qw}LFN{g~OxYx*A>_>0+wdLXZTHhHZi~KAXOSdU%Uj3HLxs5^<6oCA4_02W10G5EKwCAE%%okenP>NoK0+|Y z^4v(Aqo3=yr7+c#_c(6lnj=3Nj3o^BIC>$v?)Tf?YHNcv~6T`;mG&qc@XKB z31c8;0RJnc(YK2d;vdn^5GUxG%sz0WFs^w$mRl;}p4Q5Z+p)B3Xn?2Jf$%)P+(8KP zo=u1v;3oiK7HOV*nJ|Y+-CA2g0o;n3TR&%(dkNaHa=@*rb&*n!3ZjBF>Cu#E89E>- zf-T+am7x*YoMKYw7aNYyXxI9^L*Ke7K5cGvBaDLJn8bdMpf^k-9cJLyG zLfd;S%JgIjEdyU`o9;boFtzZk^*JKBo{G!h#Bv$! zMpJopvYjoY?Z)XwZP=++eFJcjYon~?0?38b!;_9a-*0{U4!^H-cT+;Y+R(GP`T{~v z^MepeI{JSWDTu3xQTdGDBTw>ymh*{M+Mb|ZIvRTZ*{Z=b9v)+Vjb zrb$13dC{EWLdtg>E5marac#X|@+3D~Ej{1GOcDu#NskY@nMm+SEeq>vO$#8kpkMc%MCQVwuN0QuL!evvaM_^YEZq;m&w9SNZUTk_(S? z1%~)FhbCti@dzZdD8@v80B>bn**uid>o!C$1{uCg_trl2+{7F9lN}-+Q*L_K)n5$z zB+@H;aU4Nta|R}IgQ)nIqP7m`l>!IyDO0MSVl{_ruAeW@VTH<%&krht=%*uqS7sm+ zEOsIyF*Q1UYTHui$=)_*CA?Hb%gUKLV>jRC;93|K)4lI+?jS>hEWADpsa0Ql=zLYb z3;9WbZvN6MOv709;rJuCc&A;#TUniwo2Mcwe@+N359L=V~}91!4S1hnW}noscA2ggSbv<2~Nsgtchx2ig;? z?FM$Ml|KNf8WyT#UzgXzcNud2b+75(h<}e8cvN+aT(tU5r*)Y0k&9%ORFOcIjIB4s z#q5!+RLl&2BKb-)mxMhO#YT%aZ;8;6M}5w0)L}MI6JwE#VkYWCM5M>X2eYT5v9O{{ zzF%^uOoK|gtu9?3%ADRyv_7m|iY)qkJiq(4b|M=X{heIlC62wvW4{NX=~LmCD-jb2 zzF#IuR!{}_Dm1Z{B81;Z|)L_hx@rorh;&-Z(qrG7^@Lg=~LEn!s(jixKP(A z2n4AkNersU%h~j;2JUN|z7*zGN8A@beNg+&UU%yW#_-LVs3qlFIU!jutgall=*!6O-#GjowDfK2wI1X$@^r7}1IfN)BRbH&x^x{!f_YL*3c2_J9vF_j zL3aKQ6eM9%XI7k^{fn3ErvDE>-K7p=Z>;!&z=L(@$v{x~4LiF1OJS)zG8J`WCAql4 zZvjiAueW!~J`qMl;a2k`6A3=&c=c{oy{h#SK)8rnSbuQnU2w;u3@3kZ94u0hDe>R0 zvqPe+J3YC=&vJ72Q!T*|GKBsdZ3r)7a^xHs^|sgyHK{Xw-M;ujN3i0$Xhyi=yN~;% zcx|Zdz7NH`5p?r!7mo zUudXH^@`CQedP+mxCPpo;9~}AK|V{jdi<_Jb+cLA=n7|#SXJ_Ac#duWpYa&lCSud53vdoiX90x|suRh7f#-D#H=v)0v zimYY3=QS>bivdP7O2^iV-e2jK$qAHe!pYg&CR$+#TX+X;mX zVT>GkWgVVp*ALd)ifVv%(XG^2?pc{n60f;FGk6~NZ#sEm-`V0F{WSNMdJ_gZm3)Wp z_|_lx%o}uMr>-$JPr6aPqbDc;J{FA;{-rK_w22=MGu<~|{P*0eS}u3_(FCde*`6B% zk98e?Ugm2TF)2$ptEGrl;b0*Vl=_mw@_AP6$85_JFDQSE2-L|pFaP^@->gRo>}NMd zGmNqaIhjVX$ubtF$|l|?*@xa+Oc04NLd%2p*NW#z@Aw~@2W7Es?e}K4%Ve;NqHNGZ z4N}NxDx1j`F>AZ{+W}lJ;;C}GJR|Qd#n{uBZx}3mI!5m8 z9NKxca3s;mqDC`Ad$Wf71~c1^u0BieaaKXLtl9LwO;hY}yt-061j}3anwm79#O9cu z{fhCVB?x`*ceVV6!7lzRYFTr~h~P-S6&j$|`n^COO<|;~1Ckc*iY77?V<5H4 z&tZD*X;P(8r+%`9{VmRp_J&6DR}w$Tg4ZRiKS2_uAYmq9WIrT6$|*K}PIeT7IvEZ= z+=U1e#krux0wR(rQ4X^-HcsSL-q8}r4vS1_w+8l-CMW9n)Rv*s_EyRPO# zR-LV>gY8kH>_7MiO!V&vQa8#cfL%i7`1HV-(akI{Kilo`fH@I60R|>8=!lv zSf67`R~hp$y~g7_+PPZqYqI?{H7WL>pO#z3L86$|cY9T>eyIWd^63GEWKZoyq6|ZT zyeD{V#bSYu#Do=3Bt&rxxucAb4owo>lZEu`6fn;C*Q|;CEIlWyKLG#Da}nM{eqlXl zJ6+}FRR8>XC$S8ijcS zo7;R9by8W*Yz;hrGWTj_!c+cYzpg6vOAj4ey+7)u`>O7DT@9vv5sCvO$pv&@ueQ3} zC5V>+zPZqX8hf+$!CN2nk?||V9hpXbu-~?SUQp1ZRe`&m*A^;txOr`qr%8uW^IC%I zbXzGalYXRoe&+#w*7v8oD2h=OEjg1VQ+>lGG)+$E*0Z3|J)?Pj=#IAXSBxSYlIw2E zorw!@PczdZlUXu4_HCa<+y_To0Rp40+l~TKbKHnV2JVZx@vN}BkDD(@0XS@U+tnbO z%yB&yIaM__dV-h-yEi13GOLI9(+;m{YiN_zvBycwsU_aqNxY1p|h2c=*pw#8)PR#%N0+ z;;ioQGvN2ySu>Z5$?&g)iU-&W-ut5KQ6d1;LBU-E5*m68*&9^!kRvUMzh@pcqHVcjAhL|2lwv4misU?TO z>}KVlI)Mtyf~Mxa(S*q$?{lo3HN5oKlH-Dhask;)+`8!K}WaXOAIWE_H5vF@D zw#Voli=w~MJzeL6nM01dumh{TQbzU=M$DP9>7ued!%0wlWV!WZ-xPfPNl-1ZUzoDU z=2`1G*wIMvm2Q4vB?uZ3bIdHR95Cxvcf#Jvb+Z2qE@^Bs*Tx#MRJ@)wPQwN2%)CY2 zuW3pce;(2E{YK>Y&<2G)g8}ZrAO>v43b_gEVgRyO6F8)EuO1wY7=!L3?16JWay9a$ z?ASN!RA<(w79;o-uG+iLA_9l0I2r~}3@uc)cWviDKa-~(Pw?89;^STPO8AA<>KT!5Wx$;=6(@Xsf13}| zSS|lzFj9LZ0-rrD=b4R*n4C+nlva6QUCm2O`m$*i~WX;VS_mNM!~|6 z-|3fu-pV)+-D-P6PRs5s4O%A)x2^&>bFSY80V&YQ%}f7T$v%4p*zuIigaktDQz0aR z8%GX!yFIC!vP!4}s19Pa{2n|6>P&|i*op5yI` zPpDc%5OIpTi?b-@mxn)q-1S#@&qPJ=SXeNHn58%Xq|l4XK@{zN1A7VDVk%HZxW5cB zb|hGSD^b^MIBlY$@_`G($Rlmz#BxfRrg7g|Bo=2kaxgLTm&Qo-gh)TtQ~QGvVfhfU zPv4>yoi&UpBXjzCMgp0qRC>8Q zlcKuen?}Kpjw<*^+`^*TIJ6=%L2ncxAaRdvL?=fN?Vh%C>*;)+pn^nBw_jIy2}g9D z?$UR_r##)n_H9OA_4h$q+aR81;zd_pQdK+z7yo<&SHeoWncE0Oaw1nmdew^+ON zxD>61K8BnueXEXs z;0+{VR2$LU2su3V@rs;#%Sts+oO7fVu1TF*Iqk3f0{7iBWQ@DgDV&uk zdzil(WLeEpt zmV(x#vpm*Tjy6*(w!6+gia4uvFj=8r+;Ry>C1n+TR$o6QqmV4#B~Ma9lO4XVns{mi zq!RovL{@wn#_{j_c^5XjY5IK^vEkXUMB(%LkClLDwRh6VFwstFf3!EKv^Gu6eBtAx zBaH7yG319&1N@=5uo-~%ShQ7r`}q&D2IGT1fMG6!t=s9@&1JE0JNkb^yz7;Oi3p4%hd>yMUiL1-%wls82 zLW9pE7ft(i)NReI@J*W4-cuf8vt%T(ySx|AmT@FZqiCy>#ez+xGx6#951^to&4l&+ z7}pt|%dk6MoISE1yFu3F)Gv}d1g8Nk7t~zrbpCK}<*~vs%(YG=V2Ic1AHWr7-~zSS zg$G0ef0uGsNnKB*D5L6`OwmBpS$GYR_?S+68PcXZ?D?tzh zUa#+=!scNDy{G;FT5DonY-1OHIA3O!U67lTeCls-1L{ms*#(73t)+tjK;Tc_=v9sZ zB*CH6elc-$H16&b?7G~J5MSu+pmphDF#jH`H13)sZ2U3`>1pHlVge?qN^6_UAzO~e z`>`as44rDJsI8Q*b<#m8XN?T45^#$FD5g_5xTI&&`ju54aR6Ajr!LX7GOemW2|6W8^!70m!3I2%ky)=)+o{l(7$32f)x3_bhnse1i`p8_oQitcn94 zP_LYZQB;sPhE!s|vte?DH4PW=)BOQ_?W7yl9Xjb5jk=))20OR9?2QHu?2TS;5L|Mk zj`h66V^I0qP6G2Qe$8uiMv3bqT>$%;!t}6IXNIRpaH~19GmHtyYEygJFA=i^Ug~^r zD-wrz-(u=gdm01D#HW`Hs}SM5vsstvDdK*kjMbwv09Umi2ej~uuanvQkdWr8x}f7` z_i6Y@PTjA*l{kJ1E3`*LGYTYqU{}{qInBfZP*;+lca-=gEmW)M>NN~O8@tRO2kdR0 z{a-3ZNx9hhL_h`)$2?p@o07q`DDAD7pEQXB1ntOQbAUO1l0;*wVW}RwfMpv&V|Bf@ z@0N2;j)=Xz2+wJqeQ7FRXU2uIp^QERVUv_8_R<+CU3azz#^pc_Zwtf)S{(-)$+s{Q;=We#wKMn~MI5d!V4ONV_m+VAF;=Y0hcd653?JU?VhK zTfS7SVF5yBd0eEcx38oG(SvEZyK(Uu!i8v%tVBZ!NhqUcDVj!~tnn#Y*M4z=1k6S1 zB7&<3P7o-TitlQ@$K@sCzTJjKsK_NWIK03_m@8&G&p355_W{9eKKV=>Fr~Rjs<|L~ zD>CF|;siODncqkfVl5+ySvSmGHW_m^r5SOWL77(&mCch2Ov{dyNR5~auB`<_Vae>! zh)%3+tW31TqwSmWx)|0g!-Y)bcFO?UyTe;_es(u=UR_~rc0F__hc)A=AId+}6ZfC| zN)8DDQeOEM>=CC^(h<%kWE$e;d=nOBm%1nI zLsV8{8i;D2C~9|yx!nn5)hU^pZJl@=Q94qpV0T7E8qXQFKY_Al%9>FB;moevv*JhDY=s%?zG(_#+^P3RphPNhK#+o!F8+-W0XJ7W+q=?d%J}z4rqB+j! zSH3x%5r7|KvG=BLd#RmOTGlh;#=&IW$wRSQyK*mFHsspMun0!q7n2lRShwl&ex}%j ziZ)@oh3d^-nfoM6puwZvWBv|^2JL7L>=NVh);a=zM>&Y=k@U@`KG^F9ZomCbYE+Ch zqE-CK{uqO(0FguU3gT0zucMmm!RaFrb=+F>Qt^wn9grZt?;vSlyKf4CLsfMPYq0DTWc6x{5YZRDb2LM_26MuKizgv;h2<0eb<8+X z)KKLz?z&}S+e5mV|HPERn&IU*8?&E1j#*~L`qh5!T-X?o+v7(?O}bOE&GD0NyC2Jk zQNp;&Oey6($}gNObGN!?GLjjGL*6YfNA*$mh%a)gUv9vZlHKTyD)N+3Z?{D+Z$@mP zo5lS=%_u*$>Mo^{c_Hc6DTx2o+cMxH?eky; zpV-9F^z8TKq_-%x1%2$XDv2wUG zMM0gblKm@3hwV}2U)wnt`Fz$sIB%|o_{%k6)sfa%Mw3A^cjh-`Wd9a|7Q-Bx zYPfoZ87{~;a4iFXiw?T0c#k*n!gLNbkE18lG9ktjl0;`qF`j-0e^Qe)5R;M%2VUbA z#Wo8t+VVRY@IMM36ETq472=3RbfGmmqqA&sjP!vC+@p^;cFPM&zltC;bbIxSD36<2 znb+FXqyr*1;EyzTihgXds_19dmHq&a)Xy1CA_=f{B1qQ3vU1T_k+`v?epaMVh(xTE zi73nPf6SSpK_?lw&U@}Z1Oe%V-h*XWMWBsd#bYMldd&xZ-AiF1jM-j@#k?qF3eM*G zfHB#9!TuiI??ey|&`D^$zPi<=FV=bYD=7tAIteJH5`^$(H0Ser+L=8^{(59iB||hX z$6i3bI!1Ts*frhA!_z!{f-wUX5NSi0iy~^97{ONa-DEA)EiM9zhbnnc38MdrE#&X86f_F1HeDQ#U*e&#kPRB*Q zCD>DHo^`1NQvh(@%kcA3^2wVlbDJ+gGI2K@+>974aU#Q45QG4RiWm*Xq15VPI0^{PA870EdkL?c| z*7?3IYC15vUt3+*MjuIgTH!R$llx~E8lQ->m2bXKrW|nh>glRdnR@RT+|0W)$6;i5 z=Nu@w@N9k++0hJZbY zV0KsV3Aod!s$z@FZ}q!=%t~30B$TYlG?X7DtQO#;a{zq-1gd;>7l=nHY7k5WBU75B zU-u^U^5S>5y&vkmDc?K)JR(xU@?IcAD%8g7VJ_+K?UjH)*c=4dHJF-QMYaL{7WP;v z=kCBE+E_J(P?8tyQw<&LLnyutf#!5h{W_1zEmmPQ*+a2GPEUZS ze5W;Aydcn>utf9l0y!kZ+6SRG&3yrE zPRAS&31Ki4e$kVUfNvRIOI-Oqhj1vV!*kuX?H>E)6{~KNniXV%tT9>j4P8w+tr5p< zPA>r&uTU+|njZ2#vYVZSosp4L$an~(KB}d8%sG`PfOoryCr_eK>F70WKt);Aajpl+ zRv)u)RwCwdg2dk7GTu5y87H^Q61>?8p+$)Za_REahB~>{FzJwYj4jZ_jZF z9&&FGLCgjIK=)*agPu|`dc$|mkBlj7Huk!QPhtEVkpmwNPL{tFyjE_dUOOWdYAUER zu(BfRoi>hE&1W&193)QdwnFNfCs93pgAFn3PCk}2Dt-M#TcNdZCzbO32A$)S@~fZJ zRq2mq5?|KRStrkDuW8a(Sg)PKex&aznW;K__MaLGb^2k{=ENO__FKQ{VetLv1si8D z1w5y_G7%lq)E3gF0yOq;`SS?#ISv>`>Vv4xF>Ov}VH!~N#hX1Sh7Co=wk~HU> z)yQw)Oz%gtG_TL%N;3#Fc#*Ap@+_cb$TPMv*SeBA`l<)mb#01yZS$N(|` z@VW)KNqX8?0)T=7@D2a~1OOI92*3gW00IC&4n*|7x+;hcfc{H|0stti|ItAJF!1un z0$>0DpbPv%Wd0}SuU~0LM=N^^a#;s6W^y(bRxSW|-3Benii;a5t13y!%1Z(u5CDLN z{sh9X0Ki8FHy2fDF>-AkU2^!%e~j@*Ow3%Jfd5-S0B}G558oN4|37~Jmmr#3xS9a~ za0vhawTrfnE&zbA0RX_Z`iD;UM>qY4F7QY9aC3440Fd$@-9=qh8~{M(007Wg{Y^Lh zn{MXh^2a|0004oH_MgoDnKyu9;h>@RSM>pa7;ptt0ck+&&-wqu#W5cMc-IUox-0A>I)Kn}10EC4IO^|}Ix18^{~aIi3NaIkRj z@Nfu7=txM2h)CF|Xej7-*!cK(*tocaBs64%L{!AMxa5rFRJ3pD-_aA0F|jbwvCz=b z)BOPf!NbENAs}HPAz{%G;u6yRf0oxC00RMP6KWG0LA2!KL|Mh5`Uap3<4QuXZ1{wenQP8JEy!H++d zCg%Uk*Lo!doH1iT@Ta-;jIsW8TKMv-e-8X;T!U+Mq5bbspq+KP1<2#6!^#XnQWpT2 z)$%bZZ#XR#sI0o3HU7Dz@pm*vOdD-GS_XD=|2qcZz+|DOf91I6k5+h|_2WwsfLBZU zhx_3T3R6uRZOO-D4G{n|!9M?FUw=_qJ7~o@9`oOU;6lG2>N+aNRJg5wE}B1IaHj*_ zs11-gFTcoRAHUIZTsV)Z0dx#qnMmG}#nUEB>F!UJpZy7hHe%CST#f$^Kn}qh@ydb5 z&YZ|d;*DIX;9_~T()fIrmx6IVBN0~+U&@kvxth`{D3m3+#*fS#U(o9|eSWI^3<&^` zB=rn*vlzK6h(G?9Gp7Ans3~o-;Eur(Ik0Pf@EgmhOG~78pEC)q{W!z$b0A>GbmP5M zb3;_wW}ob6=z`#VLtlADI5C!sK+9PnX~CL-t65P?s#1BX`+e1OV}UH%vr?GT>2!E2PtRbb7WG=(R1J{n2|0A9Y6icjSY@nrShxp&!(eKSomlR zM{zZ?V>)I9w#^m6ACO-G{NoX?8x>IiK+Iv2!^DCyu3Nr>Jcib$#v?|eCxGl$ca|)x z8XEODHd!%hj#>m47~Pfs-ak_#shrpuv-ul6#NsnNYuqVUaV)#30E>M$c{NCx~h*2GK07^un2XiEaipOyLn~$x$e0)5Bfk|nIx6TgRY!bQZg8H|v;#%w&4Qh^7 zKTQ%Q&(AVe=RwKbh`;$Z7o&PyR$r`jYCTM^L4$A`BrlI%*H_hRQn5u3_?_q3X*%+bu%fDVENZP%uVSOtf zE!1eF?ZWuyt3=Hrk}L;}bTf0T=iI-eNLJc%$pC*S`@ek!0r46twkovc<$j={LZQUHwMxF}) z{ow_Y3OxiBI12dAHrgv{cX_4GCj8G4#1izsLM?nu9sL{=?OiTVdFP^k{98N#73$no z`{`p0htE|h`JeesDgYoQ0)YSkBdwiPQFVNrB z%qr*m@iZ@-^Few*=-IR8o3mpnT_P3$&;kI>wqV7%l5r}47=-}9s0^kN8-Z&LgD2h} z?-UVL8%_%S>a0H05&G^oWcRPY&1-lnV6K%3O~Mx0?L$n#j1)7(L|0 zBmn4ssB_j?s`rjNCKC;1B>ct`4>7+HFZdtVZpDOLa`NePx@_+mN$Vz04z6^^c?a}P zcpE|`+Dy+-zJ=c3W!+@m1 zj*X7PB1@iO_c@~Lyv}vS;K_B+N4(c)< z2WoOXap8GVgPIXL5-6@lEk4})`!fuL1?5%zVc%zO27OXz&)|KEW1RK`N&Du%^+