Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Doni

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 2 additions & 1 deletion bx/build/build_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package build
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
Expand Down Expand Up @@ -32,7 +33,7 @@ func TestIntegration_SocketTriggeredBuild_LocalOutput(t *testing.T) {
require.NoError(t, err)

// 2. Setup et démarrer le Serveur Socket
socketServer := socket.NewServer(buildService, buildService) // buildService implémente les deux interfaces
socketServer := socket.NewServer(buildService, buildService, func(r *http.Request) bool {return true}) // buildService implémente les deux interfaces
socketServer.Run()
httpServer := httptest.NewServer(socketServer)
defer httpServer.Close()
Expand Down
131 changes: 0 additions & 131 deletions bx/build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,6 @@ import (
)


// 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"`
Expand All @@ -81,11 +45,6 @@ type HealthCheck struct {
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
Expand Down Expand Up @@ -133,96 +92,6 @@ 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 ---

Expand Down
87 changes: 87 additions & 0 deletions bx/build/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package build

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"gopkg.in/yaml.v3"
)

// 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
}
39 changes: 39 additions & 0 deletions bx/build/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package build

import "gopkg.in/yaml.v3"

// 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
}
20 changes: 20 additions & 0 deletions bx/build/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package build

import "context"

// Interface for an extern secrets service provider
type SecretFetcher interface {
GetSecret(ctx context.Context, source string) (string, error) // Must return the secret value
}

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)
}
18 changes: 3 additions & 15 deletions bx/build/socket.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
package build

import (
// ... autres imports ...
"context"
"encoding/json"
"fmt"
"io"
"log" // Pour les logs internes
"log" // For internal logs
"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/docker/docker/pkg/jsonmessage"
"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 ---

Expand All @@ -40,7 +28,7 @@ type logNotifierWriter struct {
buildID string
stream string // "stdout" or "stderr"
notifier socket.BuildNotifier
mu sync.Mutex // Protéger les appels concurrents potentiels à Write
mu sync.Mutex
}

func newLogNotifierWriter(buildID string, stream string, notifier socket.BuildNotifier) *logNotifierWriter {
Expand Down
2 changes: 1 addition & 1 deletion bx/build/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ type ComposeBuild struct {
CacheFrom []string `yaml:"cache_from,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
Network string `yaml:"network,omitempty"`
}
}
Loading
Loading