Go · yaml

Working with YAML

I spent my Christmas break starting work on a tool that I want to use at work. The tool is a docker-compose like application, but intended to pass information to Podman.

docker-compose (or as it’s now named docker compose takes configuration information from a compose.yml or docker-compose.yml file (other named files can be used, but these are the defaults).

It’s a simple Extract Transform Load process, Extract the information from the YAML files, validate and Transform it into commands that podman will understand, then Load those commands (via a unix socket) into podman which will then go and do its magic.

Will I finish the project? Who knows :)

The first job, then, is extraction of the configuration information from the compose.yml or docker-compose.yml (or file[s] specified by the -f or --file flag on the commandline). There is an agreed structure to the YAML, that is, the sections, headings and types are specified in advance, and they can be found in the Compose spec.

That’s a very big document, with links to more documents for sub-sections.

I have created a collection of go structs that were meant to hold the data found in the YAML files. The process of converting the YAML to go struct(s) is called unmarshal (Note, for now I am linking to gopkg.in/yaml.v2 but I have not as yet decided whichYAML libraries will be used in the project.

The structs look something like this composeyaml.go :

type Config struct {
	File     string `yaml:"file"`
	External bool   `yaml:"external"`
	Name     string `yaml:"name"`
}

type Secret struct {
	File        string `yaml:"file,omitempty"`
	Environment string `yaml:"environment,omitempty"`
	External    bool   `yaml:"external,omitempty"`
	Name        string `yaml:"name,omitempty"`
}

type Compose struct {
	Version  string             `yaml:"version,omitempty"`
	Services map[string]Service `yaml:"services"`
	Networks []Network          `yaml:"networks,omitempty"`
	Volumes  []Volume           `yaml:"volumes,omitempty"`
	Configs  []Config           `yaml:"configs,omitempty"`
	Secrets  map[string]Secret  `yaml:"secrets,omitempty"`
}

Anonymous inner structs

In my first iteration I used anonymous inner structs in several places.

eg.:

type Service struct {
	Image  string            `yaml:"image,omitempty"`
	Ports  MapOrListOfString `yaml:"ports,omitempty"`
	Deploy struct {
		EndpointMode string            `yaml:"endpoint_mode,omitempty"`
		Labels       map[string]string `yaml:"labels,omitempty"`
		Mode         string            `yaml:"mode,omitempty"`
		Placement    struct {
			Constraints map[string]string `yaml:"constraints,omitempty"`
			Preferences map[string]string `yaml:"preferences,omitempty"`
		} `yaml:"placement,omitempty"`
		Replicas  int32 `yaml:"replicas,omitempty"`
		Resources struct {
			Limits       Resources `yaml:"limits,omitempty"`
			Reservations Resources `yaml:"reservations,omitempty"`
		} `yaml:"resources,omitempty"`
		RestartPolicy struct {
			Condition   string        `yaml:"condition,omitempty"`
			Delay       time.Duration `yaml:"delay,omitempty"`
			MaxAttempts int           `yaml:"max_attempts,omitempty"`
			Window      time.Duration `yaml:"window,omitempty"`
		} `yaml:"restart_policy,omitempty"`
		RollbackConfig DeployConfig `yaml:"rollback_config,omitempty"`
		UpdateConfig   DeployConfig `yaml:"update_config,omitempty"`
	} `yaml:"deploy,omitempty"`

When I started (validation) testing that the examples found in the spec loaded into my structs (as can be seen in composeyaml_test.go I had an issue where the runtime would error, telling me that I could not use the structs in the tests as the structs that were being tested, and the reason for that was that I was not copying the annotations correctly. That is, the names and types of the fields in the structs (and anonymous inner structs) in my tests were correct, but the annotations were missing, so the runtime could not use them.

Secondly, every test I had to copy and paste the relevant parts of the struct into the tests, making for an error prone mess.

The solution: Always use exportable structs for the inner structs.

Multitype fields

The spec allows multiple types for some of the fields, for example, the Ports field can be either a map[string]string or a []string; Go, as you know isn’t a fan of dynamic typing. I had hoped generics would help here but

type MapOrListOfString interface {
    map[string]string | []string
}

produced errors about constraints not being allowed.

For now I am using interface{} with the intention of either setting the type in bespoke unmarshal function,or a separate validation function.

Note: Some time after I started the project I discovered the JSON description of the spec, which can be used with JSON-to-Go to automagically create the structs. I haven’t used it except to check my work. I wouldn’t have had nearly as much learnings had I opted to use that tool.

Published:
comments powered by Disqus