compose-go

repository·main·Indexed 19 days ago

https://github.com/compose-spec/compose-go

A Go reference library for parsing and loading Compose files according to the official Compose specification. Used by tools like Docker Compose and nerdctl, it provides a multi-phase pipeline for YAML parsing, variable interpolation, schema validation, and resource resolution to transform Compose files into validated Go structs.

Tokens
2.8K
Snippets
12
Records
19
Agent score
66%

What's inside compose-go

  1. Interpolation of variables in Compose files

    main

    Compose supports bash-style syntax for variable interpolation. This happens early in the parsing process so that the resulting values can be correctly validated against the JSON schema. Variables are resolved using values defined as environment during parsing.

    Example conversion:

    services:
      foo:
        image: "foo:${TAG}"

    becomes

    services:
      foo:
        image: "foo:1.2.3"
  2. Handling extensions (`x-*`) in Go bindings

    main
    Extension attributes (those starting with x-) can be used anywhere in a YAML document. To simplify unmarshalling into Go structs, the parser moves all extension attributes into a custom #extension attribute.
  3. Merge logic for Compose overrides

    main

    When loading an override file, the YAML tree is merged with the main Compose file. The general strategy is _append to lists, replace in mapping_, with specific rules:

    • Shell commands: Always replaced by the override.
    • options: Only merged if both files declare the same driver. Otherwise, the override fully replaces the original.
    • !reset: Can be used to remove specific elements from the original definition.
    • Type conversion: Attributes that can be expressed as both a mapping and a sequence are converted to equivalent data structures to allow merging.
  4. Use the `extends` attribute to reuse services

    main

    The extends attribute allows a service to be defined based on another existing service. The parser clones the base service's YAML subtree and then merges the local service definition as an override.

    You can use the !reset tag to explicitly remove an element from the original service definition during the merge process.

  5. How Compose file parsing works

    main

    The compose-go library processes Compose files through a multi-phase pipeline to transform raw YAML into a validated, canonical Go representation. The process follows these key stages:

    1. YAML Parsing: Uses go-yaml to handle anchors and aliases.
    2. Key Conversion: Converts all mapping keys to strings (e.g., true becomes "true").
    3. Interpolation: Resolves bash-style variables (e.g., ${TAG}) using environment variables.
    4. Schema Validation: Validates the interpolated tree against the Compose specification JSON schema.
    5. Resource Resolution: Handles extends (cloning and overriding), include (merging third-party files), and merge overrides (combining main files with override files).
    6. Unicity & Logical Validation: Enforces rules like unique volume mount paths and ensures mutually exclusive attributes (like external: true and driver: ...) are not used together.
    7. Canonicalization: Transforms 'short' and 'long' syntaxes into a single standard format.
    8. Path Resolution: Converts relative paths into absolute paths.
    9. Go Binding: Unmarshals the final tree into Go structs using mapstructure.
  6. Parse and load Compose files with compose-go

    main

    You can use the github.com/compose-spec/compose-go/v2/cli package to parse and load Docker Compose files according to the official Compose specification.

    To load a project, use cli.NewProjectOptions to configure the file paths and loading behavior (such as including OS environment variables or .env files), then call options.LoadProject(ctx) to retrieve the project object. Once loaded, you can interact with the project, for example, by using project.MarshalYAML() to get the YAML representation of the loaded configuration.

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    
    	"github.com/compose-spec/compose-go/v2/cli"
    )
    
    func main() {
    	composeFilePath := "docker-compose.yml"
    	projectName := "my_project"
    	ctx := context.Background()
    
    	options, err := cli.NewProjectOptions(
    		[]string{composeFilePath},
    		cli.WithOsEnv,
    		cli.WithDotEnv,
    		cli.WithName(projectName),
    	)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	project, err := options.LoadProject(ctx)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Use the MarshalYAML method to get YAML representation
    	projectYAML, err := project.MarshalYAML()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	fmt.Println(string(projectYAML))
    }
  7. Resolution of relative paths

    main

    Paths defined in the Compose file are resolved into absolute paths during the parsing process. This applies to standard Compose attributes and non-standard attributes like bind mount options in the local volume driver.

    Example of a relative path in driver_opts:

    volumes:
      data:
        driver: local
        driver_opts:
          type: 'none'
          o: 'bind'
          device: './data' # This relative path is resolved to an absolute path
    volumes:
      data:
        driver: local
        driver_opts:
          type: 'none'
          o: 'bind'
          device: './data'
  8. Unmarshalling into Go structs

    main
    The final YAML tree is unmarshalled into Go structs using the mapstructure library. The decoder is configured with custom decode functions to handle type conversions. For example, string representations of byte units (e.g., 640k) or durations are automatically converted into int64 types in the Go models.
  9. Logical validation of resource attributes

    main

    The parser enforces logical rules that involve relationships between attributes. A common rule is that resources marked as external must not have resource creation parameters set.

    Error Example:

    networks:
      foo:
        external: true
        driver: macvlan # This triggers an error because external networks cannot have creation parameters
    networks:
      foo:
        external: true
        driver: macvlan
  10. Configure Compose configuration files

    main

    The primary Compose files are passed as the first argument to NewProjectOptions. These files are applied in order, following the standard Compose override logic.

    To automatically populate configuration paths from the COMPOSE_FILE environment variable, use WithConfigFileEnv(o *ProjectOptions).