/* CarterPerez-dev | 2026 compose.go */ package parser import ( "fmt" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) type ComposeFile struct { Path string Root *yaml.Node Version string Services map[string]*Service Networks map[string]*Network Volumes map[string]*Volume Secrets map[string]*Secret Configs map[string]*Config } type Service struct { Name string Node *yaml.Node Line int Image string Build *BuildConfig Ports []PortMapping Volumes []VolumeMount Environment map[string]EnvVar CapAdd []string CapDrop []string Privileged bool ReadOnly bool User string NetworkMode string PidMode string IpcMode string SecurityOpt []string Deploy *DeployConfig DependsOn []string Healthcheck *HealthcheckConfig } type BuildConfig struct { Context string Dockerfile string Args map[string]string Target string Line int } type PortMapping struct { HostIP string HostPort string ContainerPort string Protocol string Line int } type VolumeMount struct { Source string Target string Type string ReadOnly bool Line int } type EnvVar struct { Name string Value string Line int } type DeployConfig struct { Replicas int Resources *ResourceConfig Line int } type ResourceConfig struct { Limits *ResourceLimits Reservations *ResourceLimits } type ResourceLimits struct { CPUs string Memory string Pids int } type HealthcheckConfig struct { Test []string Interval string Timeout string Retries int StartPeriod string Disable bool Line int } type Network struct { Name string Driver string External bool Line int } type Volume struct { Name string Driver string External bool Line int } type Secret struct { Name string File string External bool Line int } type Config struct { Name string File string External bool Line int } func ParseComposeFile(path string) (*ComposeFile, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading compose file: %w", err) } return ParseComposeBytes(path, data) } func ParseComposeBytes(path string, data []byte) (*ComposeFile, error) { var root yaml.Node if err := yaml.Unmarshal(data, &root); err != nil { return nil, fmt.Errorf("parsing yaml: %w", err) } cf := &ComposeFile{ Path: path, Root: &root, Services: make(map[string]*Service), Networks: make(map[string]*Network), Volumes: make(map[string]*Volume), Secrets: make(map[string]*Secret), Configs: make(map[string]*Config), } cf.extract() return cf, nil } func (cf *ComposeFile) extract() { if cf.Root.Kind == yaml.DocumentNode && len(cf.Root.Content) > 0 { cf.extractFromMapping(cf.Root.Content[0]) } else if cf.Root.Kind == yaml.MappingNode { cf.extractFromMapping(cf.Root) } } func (cf *ComposeFile) extractFromMapping(node *yaml.Node) { for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "version": cf.Version = value.Value case "services": cf.extractServices(value) case "networks": cf.extractNetworks(value) case "volumes": cf.extractVolumes(value) case "secrets": cf.extractSecrets(value) case "configs": cf.extractConfigs(value) } } } func (cf *ComposeFile) extractServices(node *yaml.Node) { if node.Kind != yaml.MappingNode { return } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } name := node.Content[i].Value serviceNode := node.Content[i+1] service := cf.parseService(name, serviceNode) cf.Services[name] = service } } func (cf *ComposeFile) parseService(name string, node *yaml.Node) *Service { svc := &Service{ Name: name, Node: node, Line: node.Line, Environment: make(map[string]EnvVar), } if node.Kind != yaml.MappingNode { return svc } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "image": svc.Image = value.Value case "build": svc.Build = cf.parseBuildConfig(value) case "ports": svc.Ports = cf.parsePorts(value) case "volumes": svc.Volumes = cf.parseVolumeMounts(value) case "environment": svc.Environment = cf.parseEnvironment(value) case "cap_add": svc.CapAdd = cf.parseStringList(value) case "cap_drop": svc.CapDrop = cf.parseStringList(value) case "privileged": svc.Privileged = value.Value == "true" case "read_only": svc.ReadOnly = value.Value == "true" case "user": svc.User = value.Value case "network_mode": svc.NetworkMode = value.Value case "pid": svc.PidMode = value.Value case "ipc": svc.IpcMode = value.Value case "security_opt": svc.SecurityOpt = cf.parseStringList(value) case "deploy": svc.Deploy = cf.parseDeployConfig(value) case "depends_on": svc.DependsOn = cf.parseDependsOn(value) case "healthcheck": svc.Healthcheck = cf.parseHealthcheck(value) } } return svc } func (cf *ComposeFile) parseBuildConfig(node *yaml.Node) *BuildConfig { bc := &BuildConfig{Line: node.Line} if node.Kind == yaml.ScalarNode { bc.Context = node.Value return bc } if node.Kind != yaml.MappingNode { return bc } bc.Args = make(map[string]string) for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "context": bc.Context = value.Value case "dockerfile": bc.Dockerfile = value.Value case "target": bc.Target = value.Value case "args": bc.Args = cf.parseStringMap(value) } } return bc } func (cf *ComposeFile) parsePorts(node *yaml.Node) []PortMapping { var ports []PortMapping if node.Kind != yaml.SequenceNode { return ports } for _, item := range node.Content { pm := PortMapping{Line: item.Line} switch item.Kind { case yaml.ScalarNode: pm = cf.parsePortString(item.Value, item.Line) case yaml.MappingNode: pm = cf.parsePortMapping(item) } ports = append(ports, pm) } return ports } func (cf *ComposeFile) parsePortString(s string, line int) PortMapping { pm := PortMapping{Line: line, Protocol: "tcp"} if idx := strings.LastIndex(s, "/"); idx != -1 { pm.Protocol = s[idx+1:] s = s[:idx] } parts := strings.Split(s, ":") switch len(parts) { case 1: pm.ContainerPort = parts[0] case 2: pm.HostPort = parts[0] pm.ContainerPort = parts[1] case 3: pm.HostIP = parts[0] pm.HostPort = parts[1] pm.ContainerPort = parts[2] } return pm } func (cf *ComposeFile) parsePortMapping(node *yaml.Node) PortMapping { pm := PortMapping{Line: node.Line, Protocol: "tcp"} for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1].Value switch key { case "target": pm.ContainerPort = value case "published": pm.HostPort = value case "host_ip": pm.HostIP = value case "protocol": pm.Protocol = value } } return pm } func (cf *ComposeFile) parseVolumeMounts(node *yaml.Node) []VolumeMount { var mounts []VolumeMount if node.Kind != yaml.SequenceNode { return mounts } for _, item := range node.Content { vm := VolumeMount{Line: item.Line} switch item.Kind { case yaml.ScalarNode: vm = cf.parseVolumeString(item.Value, item.Line) case yaml.MappingNode: vm = cf.parseVolumeMountMapping(item) } mounts = append(mounts, vm) } return mounts } func (cf *ComposeFile) parseVolumeString(s string, line int) VolumeMount { vm := VolumeMount{Line: line, Type: "bind"} parts := strings.Split(s, ":") switch len(parts) { case 1: vm.Target = parts[0] vm.Type = "volume" case 2: vm.Source = parts[0] vm.Target = parts[1] case 3: vm.Source = parts[0] vm.Target = parts[1] if parts[2] == "ro" { vm.ReadOnly = true } } if strings.HasPrefix(vm.Source, "/") || strings.HasPrefix(vm.Source, ".") { vm.Type = "bind" } else if vm.Source != "" { vm.Type = "volume" } return vm } func (cf *ComposeFile) parseVolumeMountMapping(node *yaml.Node) VolumeMount { vm := VolumeMount{Line: node.Line} for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "type": vm.Type = value.Value case "source": vm.Source = value.Value case "target": vm.Target = value.Value case "read_only": vm.ReadOnly = value.Value == "true" } } return vm } func (cf *ComposeFile) parseEnvironment(node *yaml.Node) map[string]EnvVar { env := make(map[string]EnvVar) if node.Kind == yaml.MappingNode { for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i] value := node.Content[i+1] env[key.Value] = EnvVar{ Name: key.Value, Value: value.Value, Line: key.Line, } } } else if node.Kind == yaml.SequenceNode { for _, item := range node.Content { parts := strings.SplitN(item.Value, "=", 2) name := parts[0] value := "" if len(parts) > 1 { value = parts[1] } env[name] = EnvVar{ Name: name, Value: value, Line: item.Line, } } } return env } func (cf *ComposeFile) parseDeployConfig(node *yaml.Node) *DeployConfig { dc := &DeployConfig{Line: node.Line} if node.Kind != yaml.MappingNode { return dc } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "replicas": dc.Replicas, _ = strconv.Atoi(value.Value) case "resources": dc.Resources = cf.parseResourceConfig(value) } } return dc } func (cf *ComposeFile) parseResourceConfig(node *yaml.Node) *ResourceConfig { rc := &ResourceConfig{} if node.Kind != yaml.MappingNode { return rc } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "limits": rc.Limits = cf.parseResourceLimits(value) case "reservations": rc.Reservations = cf.parseResourceLimits(value) } } return rc } func (cf *ComposeFile) parseResourceLimits(node *yaml.Node) *ResourceLimits { rl := &ResourceLimits{} if node.Kind != yaml.MappingNode { return rl } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "cpus": rl.CPUs = value.Value case "memory": rl.Memory = value.Value case "pids": rl.Pids, _ = strconv.Atoi(value.Value) } } return rl } func (cf *ComposeFile) parseHealthcheck(node *yaml.Node) *HealthcheckConfig { hc := &HealthcheckConfig{Line: node.Line} if node.Kind != yaml.MappingNode { return hc } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } key := node.Content[i].Value value := node.Content[i+1] switch key { case "test": hc.Test = cf.parseStringList(value) case "interval": hc.Interval = value.Value case "timeout": hc.Timeout = value.Value case "retries": hc.Retries, _ = strconv.Atoi(value.Value) case "start_period": hc.StartPeriod = value.Value case "disable": hc.Disable = value.Value == "true" } } return hc } func (cf *ComposeFile) parseStringList(node *yaml.Node) []string { var result []string if node.Kind == yaml.ScalarNode { return []string{node.Value} } if node.Kind != yaml.SequenceNode { return result } for _, item := range node.Content { result = append(result, item.Value) } return result } func (cf *ComposeFile) parseStringMap(node *yaml.Node) map[string]string { result := make(map[string]string) if node.Kind == yaml.MappingNode { for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } result[node.Content[i].Value] = node.Content[i+1].Value } } else if node.Kind == yaml.SequenceNode { for _, item := range node.Content { parts := strings.SplitN(item.Value, "=", 2) if len(parts) == 2 { result[parts[0]] = parts[1] } } } return result } func (cf *ComposeFile) parseDependsOn(node *yaml.Node) []string { if node.Kind == yaml.SequenceNode { return cf.parseStringList(node) } if node.Kind == yaml.MappingNode { var deps []string for i := 0; i < len(node.Content); i += 2 { deps = append(deps, node.Content[i].Value) } return deps } return nil } func (cf *ComposeFile) extractNetworks(node *yaml.Node) { if node.Kind != yaml.MappingNode { return } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } name := node.Content[i].Value netNode := node.Content[i+1] net := &Network{Name: name, Line: netNode.Line} if netNode.Kind == yaml.MappingNode { for j := 0; j < len(netNode.Content); j += 2 { if j+1 >= len(netNode.Content) { break } key := netNode.Content[j].Value value := netNode.Content[j+1] switch key { case "driver": net.Driver = value.Value case "external": net.External = value.Value == "true" } } } cf.Networks[name] = net } } func (cf *ComposeFile) extractVolumes(node *yaml.Node) { if node.Kind != yaml.MappingNode { return } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } name := node.Content[i].Value volNode := node.Content[i+1] vol := &Volume{Name: name, Line: volNode.Line} if volNode.Kind == yaml.MappingNode { for j := 0; j < len(volNode.Content); j += 2 { if j+1 >= len(volNode.Content) { break } key := volNode.Content[j].Value value := volNode.Content[j+1] switch key { case "driver": vol.Driver = value.Value case "external": vol.External = value.Value == "true" } } } cf.Volumes[name] = vol } } func (cf *ComposeFile) extractSecrets(node *yaml.Node) { if node.Kind != yaml.MappingNode { return } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } name := node.Content[i].Value secNode := node.Content[i+1] sec := &Secret{Name: name, Line: secNode.Line} if secNode.Kind == yaml.MappingNode { for j := 0; j < len(secNode.Content); j += 2 { if j+1 >= len(secNode.Content) { break } key := secNode.Content[j].Value value := secNode.Content[j+1] switch key { case "file": sec.File = value.Value case "external": sec.External = value.Value == "true" } } } cf.Secrets[name] = sec } } func (cf *ComposeFile) extractConfigs(node *yaml.Node) { if node.Kind != yaml.MappingNode { return } for i := 0; i < len(node.Content); i += 2 { if i+1 >= len(node.Content) { break } name := node.Content[i].Value cfgNode := node.Content[i+1] cfg := &Config{Name: name, Line: cfgNode.Line} if cfgNode.Kind == yaml.MappingNode { for j := 0; j < len(cfgNode.Content); j += 2 { if j+1 >= len(cfgNode.Content) { break } key := cfgNode.Content[j].Value value := cfgNode.Content[j+1] switch key { case "file": cfg.File = value.Value case "external": cfg.External = value.Value == "true" } } } cf.Configs[name] = cfg } } func (cf *ComposeFile) Visit(visitor ComposeVisitor) { for name, svc := range cf.Services { visitor.VisitService(name, svc) } } type ComposeVisitor interface { VisitService(name string, svc *Service) }