diff --git a/.encryption-service/service.log b/.encryption-service/service.log deleted file mode 100644 index 5ebb40f..0000000 --- a/.encryption-service/service.log +++ /dev/null @@ -1,4 +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"} 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/.gitignore b/.gitignore index 7f4729a..d764f33 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,8 @@ go.work.sum .vscode client -node_modules +/**/node_modules test.db bin -rsa/ \ No newline at end of file +rsa/ +/**/**TODO.md \ No newline at end of file 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 141417c..224397d 100644 --- a/Makefile +++ b/Makefile @@ -45,13 +45,17 @@ 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: @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/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. 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/api/routes.go b/api/routes.go deleted file mode 100644 index 62ad546..0000000 --- a/api/routes.go +++ /dev/null @@ -1,28 +0,0 @@ -package api - -import ( - "log" - - "cloudbeast.doni/m/controllers" - "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) { - 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/atlas.hcl b/atlas.hcl deleted file mode 100644 index 0a4cc2b..0000000 --- a/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/build/builder.go b/build/builder.go deleted file mode 100644 index 66457c8..0000000 --- a/build/builder.go +++ /dev/null @@ -1,145 +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/bx/build/build_integration_test.go b/bx/build/build_integration_test.go new file mode 100644 index 0000000..fed94eb --- /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. + "github.com/Treefle-labs/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 new file mode 100644 index 0000000..2a95b24 --- /dev/null +++ b/bx/build/builder.go @@ -0,0 +1,2005 @@ +package build + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "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/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + "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" + + // mod for B2 + "github.com/Backblaze/blazer/b2" +) + +// --- 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"` // 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 +} + +// Representation of any codebase in the services +type CodebaseConfig struct { + 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 is a Docker build config spec extended +type BuildConfig struct { + 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 is the struct representing a build result of each service +type BuildResult struct { + 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 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"` + BucketName string `json:"bucket_name" yaml:"bucket_name"` + BasePath string `json:"base_path" yaml:"base_path"` +} + +// The Main service to manage each build +type BuildService struct { + 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 +} + +// --- 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("error during the Docker client initialization: %w", err) + } + + // 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("working dir creation failed %s: %w", workDir, err) + } + } + + return &BuildService{ + dockerClient: cli, + workDir: effectiveWorkDir, + inMemory: inMemory, + secretFetcher: secretFetcher, // Inject the secret fetcher + mutex: sync.Mutex{}, + }, nil +} + +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 +} + +// --- 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("cannot read the build file specification '%s': %w", filename, err) + } + return LoadBuildSpecFromBytes(data, filepath.Ext(filename)) +} + +// 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 { + // 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("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 +} + +// 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), // 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 + + // --- 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 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 { + 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 + } + } + } + } + // 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)) + } + } + + // --- 5. Prepare Codebases --- + overallLogs.WriteString("Fetching codebases...\n") + codebaseMap := make(map[string]CodebaseConfig) // For easy lookup by name + for _, codebase := range spec.Codebases { + 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 = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + } + + // --- 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 = 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 { + // --- 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 + 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 = errMsg + result.Logs = overallLogs.String() + return result, fmt.Errorf("error during the run: \n %s", errMsg) + } + + // 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)) + } + + // --- 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)) + } + } + } + + // 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)) + } + } + + 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) + } + + // --- 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 { + 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 { + 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 +} + +// --- Helper Functions --- + +// fetching codebase from the provided source type and config +func (s *BuildService) fetchCodebase(ctx context.Context, config CodebaseConfig, destDir string) error { + // 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": + // 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": + if len(config.Content) == 0 { + 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("this source type is not implemented yet '%s' for the codebase '%s'", config.SourceType, config.Name) + } +} + +// 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 + } + + if config.Branch != "" { + options.ReferenceName = plumbing.NewBranchReferenceName(config.Branch) + options.SingleBranch = true + options.Depth = 1 + } + + 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) + } + 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) + } + + 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 nil +} + +// Used to copy a local dir/files with appropriate permissions +func (s *BuildService) copyLocalDir(source, dest string) error { + 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 + } + + for _, entry := range entries { + sourcePath := filepath.Join(source, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + fileInfo, err := entry.Info() + if err != nil { + return err + } + + if entry.IsDir() { + // Recursively copy subdirectory + if err := s.copyLocalDir(sourcePath, destPath); err != nil { + return err + } + } 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 { + // Copy regular file content and permissions + data, err := os.ReadFile(sourcePath) + if err != nil { + return err + } + if err := os.WriteFile(destPath, data, fileInfo.Mode()); err != nil { + return err + } + } + } + return nil +} + +// Extract an archive (tar, tar.gz, zip) to a repertory +func (s *BuildService) extractArchive(sourcePath string, destDir string) error { + file, err := os.Open(sourcePath) + if err != nil { + 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) + } +} + +// Extract a buffer slice to a dir +func (s *BuildService) extractBufferToDir(data []byte, destDir string) error { + dataReader := bytes.NewReader(data) + + if bytes.HasPrefix(data, []byte{0x1F, 0x8B}) { + // Archive gzip (tar.gz) + gzr, err := gzip.NewReader(dataReader) + if err != nil { + 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 + return extractZip(dataReader, int64(len(data)), destDir) // Implement extractZip for ReaderAt + } else { + // Supposer tar simple + return extractTar(tar.NewReader(dataReader), destDir) + } +} + +// Extract a tar archive +func extractTar(tr *tar.Reader, destDir string) error { + for { + header, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + 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, info.Mode()); err != nil { + return fmt.Errorf("cannot create the repertory for the tar '%s': %w", target, err) + } + case tar.TypeReg: + // 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) + } + + // Create the file + file, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + if err != nil { + return fmt.Errorf("cannot create the tar file '%s': %w", target, 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) + } + 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 +} + +// 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 +} + +// 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) + // 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 "", 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), // 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 (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 + 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 + // } + // } + + 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 { + // 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 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 // Fin normale du stream + } + // 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) + // Try to parse image ID from common "Successfully built " messages + if strings.Contains(msg.Stream, "Successfully built ") { + parts := strings.Fields(msg.Stream) + 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) + } + + // 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 != "" { + // Prefer the ID from Aux if available + id := strings.TrimPrefix(auxMsg.ID, "sha256:") + imageID = id + } + } + } // End stream reading loop + + 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)") + } + } + + // Clean the image ID (remove potential sha256: prefix if still there) + imageID = strings.TrimPrefix(imageID, "sha256:") + + fmt.Fprintf(&logBuffer, "\nBuild successful. Final Image ID: %s\n", imageID) + return imageID, logBuffer.String(), nil +} + +// 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 + } + + overallLogs.WriteString(fmt.Sprintf("--- Building Service: %s ---\n", Name)) + + // 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) + + 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, + }, + } + + // 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? + } + } + } + + // 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)) + + 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 + } + + 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)) + } + + // Store results for this service + result.ImageIDs[Name] = imageID + result.ImageSizes[Name] = imageSize + result.ServiceOutputs[Name] = ServiceOutput{ + ImageID: imageID, + ImageSize: imageSize, + Logs: logs, + } + 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)) + + } // End loop over services + + return buildErrors +} + +// 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 + } + 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) + } + + // 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 fmt.Errorf("erreur lors du lancement du pull de l'image '%s': %w", imageName, err) + } + defer reader.Close() + + // 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 fmt.Errorf("erreur lors de la lecture du flux de pull pour l'image '%s': %w", imageName, err) + } + + fmt.Fprintf(logs, "Image '%s' pulled successfully.\n", imageName) + return nil +} + +// 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 0, fmt.Errorf("erreur d'inspection de l'image '%s': %w", imageID, err) + } + return summary.Size, nil +} + +// 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 nil, fmt.Errorf("erreur d'inspection de l'image taggée '%s': %w", imageTag, err) + } + return &summary, nil +} + +// 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'export de l'image '%s': %w", imageID, err) + } + defer reader.Close() + + file, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("impossible de créer le fichier image local '%s': %w", targetPath, err) + } + defer file.Close() + + _, 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 nil +} + +// 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 nil, fmt.Errorf("configuration B2 non définie pour upload") + } + + // 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 { + return nil, fmt.Errorf("erreur lors de l'export de l'image ID '%s': %w", imageID, err) + } + defer reader.Close() + + // Utiliser io.Pipe pour streamer directement vers B2 sans charger en mémoire (plus efficace pour grosses images) + pr, pw := io.Pipe() + + var uploadErr error + var wg sync.WaitGroup + wg.Add(1) + + // 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 + + 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 { + uploadErr = fmt.Errorf("erreur lors de l'initialisation du client B2: %w", err) + return + } + + 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 + } + + // Nom d'objet principal basé sur service et version + imageName := fmt.Sprintf("%s-%s.tar", serviceName, version) + objectPath := filepath.Join(s.b2Config.BasePath, imageName) + + obj := bucket.Object(objectPath) + writer := obj.NewWriter(ctx) + + 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 + } + + 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 + }() + + // 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) + }() + + // Attendre la fin de l'upload + wg.Wait() + + // 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) + } + if uploadErr != nil { + return nil, fmt.Errorf("erreur lors de l'upload vers B2: %w", uploadErr) + } + + // 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 + + // 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 + } + bucket, err := b2Client.Bucket(ctx, s.b2Config.BucketName) + if err != nil { + fmt.Printf("Warning: Failed to get B2 bucket for tag refs: %v\n", err) + return objectNames, nil + } + + 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) + + refContent := fmt.Sprintf("ImageID: %s\nTag: %s\nVersion: %s\nServiceName: %s\nMainObject: %s\n", + imageID, tag, version, serviceName, objectNames[0]) + + refObj := bucket.Object(tagPath) + refWriter := refObj.NewWriter(ctx) + + _, 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) + } + + return objectNames, nil +} + +// 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 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 + + // Copier le fichier/dossier depuis le conteneur + readCloser, _, err := s.dockerClient.CopyFromContainer(ctx, containerID, containerPath) + if err != nil { + return nil, fmt.Errorf("erreur lors de la copie depuis le conteneur '%s' (path: %s): %w", containerID, containerPath, err) + } + defer readCloser.Close() + + // Lire le contenu de l'archive tar retournée par CopyFromContainer + tarReader := tar.NewReader(readCloser) + + // 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) + } + if err != nil { + return nil, fmt.Errorf("erreur lors de la lecture de l'en-tête tar depuis la copie du conteneur: %w", err) + } + + // 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) + } + + // Lire le contenu du fichier + fileData, err := io.ReadAll(tarReader) + if err != nil { + return nil, fmt.Errorf("erreur lors de la lecture du contenu du fichier depuis l'archive tar: %w", err) + } + + return fileData, nil +} + +// 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 + } + + // 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 + + runYAML.Services[serviceName] = runService + } + + } 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 + } + } + + // 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. + } + + return runYAML, nil +} + +// 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) + } + // 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é + } + // 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) + + 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) + } +} + +// --- 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..c73ffb9 --- /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) + "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 + "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..4231a13 --- /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 + + "github.com/Treefle-labs/Anexis/bx/build" + "github.com/Treefle-labs/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.RunYAML + 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 new file mode 100644 index 0000000..95b0d81 --- /dev/null +++ b/bx/go.mod @@ -0,0 +1,75 @@ +module github.com/Treefle-labs/Anexis/bx + +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.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 + 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 + 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 + 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.32.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..60f39a6 --- /dev/null +++ b/bx/go.sum @@ -0,0 +1,236 @@ +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.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/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= +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/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= +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.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/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= +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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/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= +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/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.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-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-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.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.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= +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-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= +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/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..42ab7b6 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,3 @@ +module github.com/Treefle-labs/Anexis/cli + +go 1.24.2 diff --git a/cmd/build/main.go b/cmd/build/main.go deleted file mode 100644 index f6a4b3a..0000000 --- a/cmd/build/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "fmt" - - "cloudbeast.doni/m/build" -) - -func main() { - err := build.BuildAllTSFiles("./src") - 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 deleted file mode 100644 index 7a7aa3f..0000000 --- a/cmd/loader/main.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "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 diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index a8fa911..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - - "cloudbeast.doni/m/api" - "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" -) - -func main() { - err := godotenv.Load() - if err != nil { - log.Fatal("Erreur lors du chargement du fichier .env") - } - id := os.Getenv("B2_APPLICATION_KEY_ID") - key := os.Getenv("B2_APPLICATION_KEY") - - ctx := context.Background() - - // b2_authorize_account - b2, err := b2.NewClient(ctx, id, key) - if err != nil { - log.Fatalln(err) - } - - buckets, err := b2.ListBuckets(ctx) - if err != nil { - log.Fatalln(err) - } - 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) - - err = router.Run(":8080") // Utilisation du port 8080 pour HTTP - if err != nil { - log.Fatal("Error starting server: ", err) - } -} - -func init() { - utils.CreateDirectories([]string{}) -} diff --git a/cmd/server_test.go b/cmd/server_test.go deleted file mode 100644 index 2912c5d..0000000 --- a/cmd/server_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package main_test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "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) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, 200, w.Code) - assert.Equal(t, "pong", w.Body.String()) -} \ No newline at end of file diff --git a/cmd/watch/main.go b/cmd/watch/main.go deleted file mode 100644 index be49950..0000000 --- a/cmd/watch/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - "path/filepath" - - "cloudbeast.doni/m/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/controllers/auth_controller.go b/controllers/auth_controller.go deleted file mode 100644 index e1135c1..0000000 --- a/controllers/auth_controller.go +++ /dev/null @@ -1,51 +0,0 @@ -package controllers - -import ( - "net/http" - "time" - - "cloudbeast.doni/m/db" - "cloudbeast.doni/m/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) { - userID, ok := c.Get("userID") - var user models.User - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{"message": "User not authenticated"}) - return - } - - result := db.DB.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 deleted file mode 100644 index 15c597b..0000000 --- a/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/controllers/test_controller.go b/controllers/test_controller.go deleted file mode 100644 index 6a5c8b0..0000000 --- a/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") -} \ No newline at end of file diff --git a/db/db.go b/db/db.go deleted file mode 100644 index d3a1c6d..0000000 --- a/db/db.go +++ /dev/null @@ -1,39 +0,0 @@ -package db - -import ( - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -var DB gorm.DB - -// Contient la base de données du projet 'cloudbeast' gérée par `gorm ORM` -func init() { - 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") - } - - // 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) - - DB = *db -} diff --git a/default.pgo b/default.pgo deleted file mode 100644 index 8e1947a..0000000 Binary files a/default.pgo and /dev/null differ 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/go.mod b/go.mod deleted file mode 100644 index a111858..0000000 --- a/go.mod +++ /dev/null @@ -1,107 +0,0 @@ -module cloudbeast.doni/m - -go 1.22.7 - -toolchain go1.22.9 - -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 - github.com/natewong1313/go-react-ssr v0.1.15 - github.com/stretchr/testify v1.10.0 - gorm.io/driver/postgres v1.5.11 -) - -require ( - github.com/buger/jsonparser v1.1.1 // indirect - 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/mattn/go-colorable v0.1.13 // indirect - github.com/pmezard/go-difflib v1.0.0 // 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 -) - -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 - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - 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 ( - github.com/Backblaze/blazer v0.7.2 - github.com/bytedance/sonic v1.12.6 // indirect - github.com/bytedance/sonic/loader v0.2.1 // indirect - 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/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 - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.24 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/sqlite v1.5.7 // indirect - gorm.io/gorm v1.25.12 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 9c87664..0000000 --- a/go.sum +++ /dev/null @@ -1,372 +0,0 @@ -ariga.io/atlas-go-sdk v0.6.5 h1:tl0L3ObGtHjitP9N/56njjDHUrj5jJTQBjftMNwJBcM= -ariga.io/atlas-go-sdk v0.6.5/go.mod h1:9Q+/04PVyJHUse1lEE9Kp6E18xj/6mIzaUTcWYSjSnQ= -ariga.io/atlas-provider-gorm v0.5.0 h1:DqYNWroKUiXmx2N6nf/I9lIWu6fpgB6OQx/JoelCTes= -ariga.io/atlas-provider-gorm v0.5.0/go.mod h1:8m6+N6+IgWMzPcR63c9sNOBoxfNk6yV6txBZBrgLg1o= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= -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= -github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -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/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= -github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= -github.com/gin-contrib/pprof v1.5.2 h1:Kcq5W2bA2PBcVtF0MqkQjpvCpwJr+pd7zxcQh2csg7E= -github.com/gin-contrib/pprof v1.5.2/go.mod h1:a1W4CDXwAPm2zql2AKdnT7OVCJdV/oFPhJXVOrDs5Ns= -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= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -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= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -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= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -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= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -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= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -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/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/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= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= -github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= -github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= -github.com/tkrajina/typescriptify-golang-structs v0.2.0 h1:ZedWk82egydDspGTryAatbX0/1NZDQbdiZLoCbOk4f8= -github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -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= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -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= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -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= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -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= -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.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/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= -gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= -gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= -gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= -gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= -gorm.io/driver/sqlserver v1.5.2 h1:+o4RQ8w1ohPbADhFqDxeeZnSWjwOcBnxBckjTbcP4wk= -gorm.io/driver/sqlserver v1.5.2/go.mod h1:gaKF0MO0cfTq9Q3/XhkowSw4g6nIwHPGAs4hzKCmvBo= -gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -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/middleware/auth.go b/middleware/auth.go deleted file mode 100644 index adaa7a0..0000000 --- a/middleware/auth.go +++ /dev/null @@ -1,35 +0,0 @@ -package middleware - -import ( - "net/http" - - "cloudbeast.doni/m/controllers" - "github.com/dgrijalva/jwt-go" - "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 diff --git a/migrations/20241231112547.sql b/migrations/20241231112547.sql deleted file mode 100644 index 5c35d85..0000000 --- a/migrations/20241231112547.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Create "products" table -CREATE TABLE "public"."products" ( - "id" bigserial NOT NULL, - "created_at" timestamptz NULL, - "updated_at" timestamptz NULL, - "deleted_at" timestamptz NULL, - "code" text NULL, - "price" bigint NULL, - PRIMARY KEY ("id") -); --- Create index "idx_products_deleted_at" to table: "products" -CREATE INDEX "idx_products_deleted_at" ON "public"."products" ("deleted_at"); --- Create "users" table -CREATE TABLE "public"."users" ( - "id" bigserial NOT NULL, - "created_at" timestamptz NULL, - "updated_at" timestamptz NULL, - "deleted_at" timestamptz NULL, - "name" text NULL, - "email" text NULL, - "age" smallint NULL, - "birthday" timestamptz NULL, - "member_number" text NULL, - "activated_at" timestamptz NULL, - PRIMARY KEY ("id") -); --- Create index "idx_users_deleted_at" to table: "users" -CREATE INDEX "idx_users_deleted_at" ON "public"."users" ("deleted_at"); diff --git a/migrations/atlas.sum b/migrations/atlas.sum deleted file mode 100644 index a867c85..0000000 --- a/migrations/atlas.sum +++ /dev/null @@ -1,2 +0,0 @@ -h1:foZD3dzQNM6rjQag1JFWPKNU0I9Brw5+KHn+4B44OME= -20241231112547.sql h1:IClEY8RVTGlrfUnk7WQvO9rGDpWYZcKLFfl1hHvmNf0= diff --git a/models/models.go b/models/models.go deleted file mode 100644 index b247bc9..0000000 --- a/models/models.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import ( - "database/sql" - "gorm.io/gorm" - "time" -) - -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 -} - -type Product struct { - gorm.Model - Code string - Price uint -} diff --git a/models/props.go b/models/props.go deleted file mode 100644 index 5814b6e..0000000 --- a/models/props.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - - -type IndexRouteProps struct { - User string -} \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index b1b1b8a..0000000 --- a/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "anexis", - "version": "1.0.0", - "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" - }, - "keywords": [], - "author": "", - "license": "ISC", - "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" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index cc2ae81..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1323 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -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: - 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) - -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==} - 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==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@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==} - peerDependencies: - tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - - '@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' - - '@tailwindcss/typography@0.5.15': - resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' - - '@types/react-dom@19.0.2': - resolution: {integrity: sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==} - peerDependencies: - '@types/react': ^19.0.0 - - '@types/react@19.0.2': - resolution: {integrity: sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.24.3: - resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - caniuse-lite@1.0.30001690: - resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - css-selector-tokenizer@0.8.0: - resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - culori@3.3.0: - resolution: {integrity: sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - daisyui@4.12.23: - resolution: {integrity: sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ==} - engines: {node: '>=16.9.0'} - - dependency-graph@0.11.0: - resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} - engines: {node: '>= 0.6.0'} - - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.5.76: - resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fastparse@1.1.2: - resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} - - fastq@1.18.0: - resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: '>=14'} - - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-stdin@9.0.0: - resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} - engines: {node: '>=12'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - - globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - 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'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} - 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'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mini-svg-data-uri@1.4.4: - resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} - hasBin: true - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - 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==} - - 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==} - 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==} - engines: {node: '>=0.10.0'} - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - 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'} - - 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==} - - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} - 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'} - 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' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - -snapshots: - - '@alloc/quick-lru@5.2.0': {} - - '@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 - - '@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 - - '@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': - 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 - - '@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 - - '@types/react-dom@19.0.2(@types/react@19.0.2)': - dependencies: - '@types/react': 19.0.2 - - '@types/react@19.0.2': - dependencies: - csstype: 3.1.3 - - ansi-regex@5.0.1: {} - - ansi-regex@6.1.0: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - 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 - - balanced-match@1.0.2: {} - - binary-extensions@2.3.0: {} - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.24.3: - 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) - - camelcase-css@2.0.1: {} - - caniuse-lite@1.0.30001690: {} - - chokidar@3.6.0: - 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 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - 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 - - css-selector-tokenizer@0.8.0: - dependencies: - cssesc: 3.0.0 - fastparse: 1.1.2 - - cssesc@3.0.0: {} - - csstype@3.1.3: {} - - culori@3.3.0: {} - - 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 - - dependency-graph@0.11.0: {} - - didyoumean@1.2.2: {} - - dlv@1.1.3: {} - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.5.76: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - escalade@3.2.0: {} - - 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 - - fastparse@1.1.2: {} - - fastq@1.18.0: - dependencies: - reusify: 1.0.4 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fraction.js@4.3.7: {} - - fs-extra@11.2.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-stdin@9.0.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - 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: - 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 - - graceful-fs@4.2.11: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - ignore@5.3.2: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - isexe@2.0.0: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jiti@1.21.7: {} - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - lodash.castarray@4.4.0: {} - - lodash.isplainobject@4.0.6: {} - - lodash.merge@4.6.2: {} - - lru-cache@10.4.3: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mini-svg-data-uri@1.4.4: {} - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - minipass@7.1.2: {} - - mz@2.7.0: - 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: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - object-assign@4.1.1: {} - - object-hash@3.0.0: {} - - package-json-from-dist@1.0.1: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - path-type@5.0.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - pify@2.3.0: {} - - pirates@4.0.6: {} - - 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 - - postcss-import@15.1.0(postcss@8.4.49): - dependencies: - postcss: 8.4.49 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 - - postcss-js@4.0.1(postcss@8.4.49): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.49 - - postcss-load-config@4.0.2(postcss@8.4.49): - dependencies: - lilconfig: 3.1.3 - yaml: 2.6.1 - optionalDependencies: - postcss: 8.4.49 - - postcss-load-config@5.1.0(jiti@1.21.7)(postcss@8.4.49): - dependencies: - lilconfig: 3.1.3 - yaml: 2.6.1 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.4.49 - - postcss-nested@6.2.0(postcss@8.4.49): - dependencies: - postcss: 8.4.49 - postcss-selector-parser: 6.1.2 - - postcss-reporter@7.1.0(postcss@8.4.49): - dependencies: - picocolors: 1.1.1 - postcss: 8.4.49 - thenby: 1.3.4 - - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.4.49: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - pretty-hrtime@1.0.3: {} - - queue-microtask@1.2.3: {} - - react-dom@19.0.0(react@19.0.0): - dependencies: - react: 19.0.0 - scheduler: 0.25.0 - - react@19.0.0: {} - - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - require-directory@2.1.1: {} - - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.0.4: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - scheduler@0.25.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@4.1.0: {} - - slash@5.1.0: {} - - source-map-js@1.2.1: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 - - 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 - - supports-preserve-symlinks-flag@1.0.0: {} - - tailwind-scrollbar-hide@2.0.0(tailwindcss@3.4.17): - dependencies: - tailwindcss: 3.4.17 - - 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 - - thenby@1.3.4: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-interface-checker@0.1.13: {} - - unicorn-magic@0.1.0: {} - - universalify@2.0.1: {} - - update-browserslist-db@1.1.1(browserslist@4.24.3): - dependencies: - browserslist: 4.24.3 - escalade: 3.2.0 - picocolors: 1.1.1 - - util-deprecate@1.0.2: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - y18n@5.0.8: {} - - yaml@2.6.1: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - 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 - - zustand@5.0.2(@types/react@19.0.2)(react@19.0.0): - optionalDependencies: - '@types/react': 19.0.2 - react: 19.0.0 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/routes/main.go b/routes/main.go deleted file mode 100644 index 4e3dc73..0000000 --- a/routes/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package routes - -import ( - "cloudbeast.doni/m/models" - "github.com/gin-gonic/gin" - gossr "github.com/natewong1313/go-react-ssr" -) - - -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", - }, - })) - } -} \ No newline at end of file diff --git a/routes/static.go b/routes/static.go deleted file mode 100644 index 8606e3b..0000000 --- a/routes/static.go +++ /dev/null @@ -1,25 +0,0 @@ -package routes - -import ( - "net/http" - "path" - "path/filepath" - - "github.com/gin-gonic/gin" -) - -func Static(ctx *gin.Context) { - file := ctx.Param("file") - filePath := path.Join("./client", file) - absPath, err := filepath.Abs(filePath) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"pathError": "the provided data is invalid"}) - return - } - // isValidPath := fs.ValidPath(absPath) - // if !isValidPath { - // ctx.JSON(http.StatusInternalServerError, gin.H{"fileError": "an error with the file " + filePath}) - // return - // } - ctx.File(absPath) -} diff --git a/services/compression_service.go b/services/compression_service.go deleted file mode 100644 index 12e23db..0000000 --- a/services/compression_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package services - -import ( - "bytes" - "compress/gzip" - "io" -) - -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 -} - - -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 -} diff --git a/services/encryption_service.go b/services/encryption_service.go deleted file mode 100644 index 900eb9c..0000000 --- a/services/encryption_service.go +++ /dev/null @@ -1,25 +0,0 @@ -package services - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "os" -) - -func EncryptFile(fileData []byte, pubKey *rsa.PublicKey) ([]byte, error) { - 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) -} - -func DecryptFile(cipherData []byte, privKey *rsa.PrivateKey) ([]byte, error) { - 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 deleted file mode 100644 index 9bb6f4a..0000000 --- a/services/init_service.go +++ /dev/null @@ -1,32 +0,0 @@ -package services - -import ( - "log" -) - - -var ( - 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) - } - Service = service -} \ No newline at end of file diff --git a/services/secureAlgo_service.go b/services/secureAlgo_service.go deleted file mode 100644 index d6d615c..0000000 --- a/services/secureAlgo_service.go +++ /dev/null @@ -1,613 +0,0 @@ -package services - -import ( - "archive/tar" - "bytes" - "context" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/json" - "fmt" - "io" - "log" - "os" - "path/filepath" - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - "github.com/sirupsen/logrus" -) - -// 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 -} - -// 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" -} - -// 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"` -} - -// SecureEncryptionService est une version sécurisée du service de chiffrement -type SecureEncryptionService struct { - 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 -} - -// Logger global du service -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 -} - -// 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 -} - -// 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 -} - -// 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) -} - -// 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) -} - -// 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") - } -} - -// 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 -} - -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) -} - -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] -} - -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 -} - -// 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 -} - -// 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) -} - -// 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{}) -} - -// 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 -} - -// 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 -} - -// 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 -} - - -// 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 -} - - -// 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 -} -// 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, - } -} - -// 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 -} - -// 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) - } -} - - -// 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 -} diff --git a/services/services_test.go b/services/services_test.go deleted file mode 100644 index c0e3913..0000000 --- a/services/services_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package services_test - -import ( - "os" - "path" - "testing" - - "cloudbeast.doni/m/services" -) - -const ( - FILE = "../tests/input/gojo.jpeg" - OUTPUTDIR = "../tests/output" -) - -func TestCompressionService(t *testing.T) { - data, err := os.ReadFile(FILE) - if err != nil { - t.Errorf("The file %s not exist.", FILE) - } - compressedData, compressErr := services.CompressData(data) - - 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.Close() -} - - -func TestDecompressionService(t *testing.T) { - data, err := os.ReadFile(path.Join(OUTPUTDIR, "output")) - if err != nil { - t.Errorf("The file %s not exist.", path.Join(OUTPUTDIR, "output")) - } - decompressedData, decompressErr := services.DecompressData(data) - if decompressErr != nil { - t.Error("failed to decompress data...") - } - 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) - } - outputFile.Write(decompressedData) - outputFile.Close() -} \ No newline at end of file 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..36d536c --- /dev/null +++ b/socket/go.mod @@ -0,0 +1,15 @@ +module github.com/Treefle-labs/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 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/tests/input/gojo.jpeg b/tests/input/gojo.jpeg deleted file mode 100644 index acdd184..0000000 Binary files a/tests/input/gojo.jpeg and /dev/null differ diff --git a/tests/output/output b/tests/output/output deleted file mode 100644 index 5dc569f..0000000 Binary files a/tests/output/output and /dev/null differ diff --git a/tests/output/output2.jpeg b/tests/output/output2.jpeg deleted file mode 100644 index acdd184..0000000 Binary files a/tests/output/output2.jpeg and /dev/null differ diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 33451fd..0000000 --- a/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": ["./frontend/src"] -} diff --git a/utils/helpers.go b/utils/helpers.go deleted file mode 100644 index 48acf39..0000000 --- a/utils/helpers.go +++ /dev/null @@ -1,65 +0,0 @@ -package utils - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "os" - "path" - "path/filepath" -) - -// 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() - - 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 diff --git a/utils/helpers_test.go b/utils/helpers_test.go deleted file mode 100644 index a6c97d7..0000000 --- a/utils/helpers_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils_test - -import ( - "crypto/rsa" - "testing" - - "cloudbeast.doni/m/utils" -) - -var privateKey *rsa.PrivateKey -const USER = "doni" - -func TestGenerator(t *testing.T) { - key, err := utils.GenerateRSAKeys(USER) - if err != nil { - t.Errorf("Failed to generate RSA keys: %v", err) - } - privateKey = key -} \ No newline at end of file