diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e36fbc8 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/bx/build/build_integration_test.go b/bx/build/build_integration_test.go index fed94eb..0c5c544 100644 --- a/bx/build/build_integration_test.go +++ b/bx/build/build_integration_test.go @@ -4,6 +4,7 @@ package build import ( "context" "fmt" + "net/http" "net/http/httptest" "os" "path/filepath" @@ -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() diff --git a/bx/build/builder.go b/bx/build/builder.go index c0340c1..68dc4fd 100644 --- a/bx/build/builder.go +++ b/bx/build/builder.go @@ -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"` @@ -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 @@ -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 --- diff --git a/bx/build/loader.go b/bx/build/loader.go new file mode 100644 index 0000000..3ce0fe5 --- /dev/null +++ b/bx/build/loader.go @@ -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 +} diff --git a/bx/build/marshal.go b/bx/build/marshal.go new file mode 100644 index 0000000..4f04f31 --- /dev/null +++ b/bx/build/marshal.go @@ -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 +} \ No newline at end of file diff --git a/bx/build/secrets.go b/bx/build/secrets.go new file mode 100644 index 0000000..e870790 --- /dev/null +++ b/bx/build/secrets.go @@ -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) +} \ No newline at end of file diff --git a/bx/build/socket.go b/bx/build/socket.go index c73ffb9..da094d3 100644 --- a/bx/build/socket.go +++ b/bx/build/socket.go @@ -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 --- @@ -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 { diff --git a/bx/build/spec.go b/bx/build/spec.go index fa93bf2..94b4df0 100644 --- a/bx/build/spec.go +++ b/bx/build/spec.go @@ -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"` -} \ No newline at end of file +} diff --git a/socket/LICENSE b/socket/LICENSE new file mode 100644 index 0000000..e36fbc8 --- /dev/null +++ b/socket/LICENSE @@ -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. \ No newline at end of file diff --git a/socket/client.go b/socket/client.go index a9f69a9..1853a84 100644 --- a/socket/client.go +++ b/socket/client.go @@ -32,7 +32,7 @@ type Client struct { pendingMu sync.RWMutex } -// Creating anew client for a websocket connection. +// Creating a new client for a websocket connection. func NewClient() *Client { return &Client{ Incoming: make(chan *Message, 100), // Buffer for incoming messages