hclconfig

package module
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 16, 2024 License: MIT Imports: 37 Imported by: 3

README

HCL Configuration Parser

Go Reference

This package allows you to process configuration files written using the HashiCorp Configuration Language (HCL). It has full resource linking where a parameter in one configuration stanza can reference a parameter in another stanza. Variable support, and Modules allowing configuration to be loaded from local or remote sources.

The project aims to provide a simple API allowing you to define resources as Go structs without needing to fully understand the HashiCorp HCL2 library.

HCLConfig has a full AcyclicGraph that allows you to process configuration with strict dependencies. This ensures that a parameter from one configuration has been set before the value is interpolated in a dependent resource.

Parsing is a two step approach, first the parser reads the HCL configuration from the supplied files, at this stage a graph is computed based on any references inside the configuration. For example given the following two resources.

resource "postgres" "mydb_2" {
  location = "localhost"
  port = 5432
  name = "mydatabase"

  username = "db2"
  password = resource.postgres.mydb_1.password
}

resource "postgres" "mydb_1" {
  location = "localhost"
  port = 5432
  name = "mydatabase"
  
  username = "db1"
  password = random_password()
}
Step 1:

When the first pass of the parser runs it will read mydb_2 before mydb_1, marshaling each resource into a struct and calling the optional Parse method on that struct. At this point none of the interpolated properties like resource.postgres.mydb_1.password have a value as it is assumed that the referenced resources does not yet exist. At this point the parser replaces the interpolated value with a default value for the field.

Step 2:

After resources have been processed from the HCL configuration a graph of dependent resources is calculated. Given the previous example where resource mydb_2 references a property from mydb_1, the resultant graph would look like the following.

| -- resource.postgres.mydb_2
     |  -- resource.postgres.mydb_1

This graph is then walked, as each resource is processed, any referenced properties are resolved and assigned to the struct. For example, when resource.postgres.mydb_2 is processed the password field that contains a reference to resource.postgres.mydb_1 will be assigned the actual value from the linked resource.

The optional Process method on the struct is also called, where a resource may contain computed fields the user can implement these computations in Process as this will make their value available to the next node in graph.

Example

Resources to be parsed are defined as Go structs that implement the Resource interface and annotated with the hcl tag

// Config defines the type `config`
type Config struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	ID string `hcl:"id"`

	DBConnectionString string `hcl:"db_connection_string"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts *Timeouts `hcl:"timeouts,block"`
}

// Parse is called when the resource is read from the file
// you can use this method to fail the config parsing early
// if the resource has validation problems.
// 
// Any references to other resources will not have been processed at this
// point and will only have the default type value.
func (t *Config) Parse() error {
	// override default values
	if t.Timeouts.TLSHandshake == 0 {
		t.Timeouts.TLSHandshake = 5
	}
	
  if t.Timeouts.TLSHandshake > 300 {
    return fmt.Errorf("TLSHandshake timeout must be less than 300")
	}

	return nil
}

// PostgreSQL defines the Resource `postgres`
type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location"`
	Port     int    `hcl:"port"`
	DBName   string `hcl:"name"`
	Username string `hcl:"username"`
	Password string `hcl:"password"`

	// ConnectionString is a computed field and must be marked optional
	ConnectionString string `hcl:"connection_string,optional"`
}

// Process is called using an order calculated from the dependency graph
// any interpolation references to other resources will have been resolved
// at this point. 
func (t *PostgreSQL) Process() error {
	t.ConnectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", t.Username, t.Password, t.Location, t.Port, t.DBName)
	return nil
}

You can then create a parser and register these resources with it:

p := NewParser(DefaultOptions())
p.RegisterType("container", &structs.Container{})
p.RegisterType("network", &structs.Network{})

The following configuration reflects the previously defined structs. config refers to postgres through the link resource.postgres.mydb.connection_string. The parser understands these links and will process postgres first allowing you to set any calculated fields in the Process callback. config also leverages a custom function random_number, custom functions allow you to set values at parse time using go functions.

variable "db_username" {
  default = "admin"
}

variable "db_password" {
  default = "admin"
}

resource "config" "myapp" {
  // Custom functions can be created to enable functionality like generating random numbers
  id = "myapp_${random_number()}"

  // resource.postgres.mydb.connection_string will be available after the `Process` has
  // been called on the `postgres` resource. HCLConfig understands dependency and will
  // call Process in a strict order
  db_connection_string = resource.postgres.mydb.connection_string

  timeouts {
    connection = 10
    keep_alive = 60
    // optional parameter tls_handshake not specified
    // TLSHandshake = 10
  }
}

resource "postgres" "mydb" {
  location = "localhost"
  port = 5432
  name = "mydatabase"

  // Variables can be used to set values, the default values for these variables will be overridden
  // by values set by the environment variables HCL_db_username and HCL_db_password
  username = variable.db_username
  password = variable.db_password
}

To process the above config, first you need to register the custom random_number function.

// register a custom function
p.RegisterFunction("random_number", func() (int, error) {
	return rand.Intn(100), nil
})

Then you can create the config and parse the file.

// define the options for the parser
opts := hclconfig.DefaultOptions()

// Callback is executed when the parser processes a resource
opts.Callback = func(r *types.Resource) error {
  fmt.Println("Parser has processed", r.Info().Name)
}

// parse a single hcl file.
// config passed to this function is not mutated but a copy with the new resources parsed is returned
//
// when configuration is parsed it's dependencies on other resources are evaluated and this order added
// to a acyclic graph ensuring that any resources are processed before resources that depend on them.
c, err := p.ParseFile("myfile.hcl")

You can then access the properties from your types by retrieving them from the returned config.

// find a resource based on it's type and name
r, err := c.FindResource("resource.config.myapp")

// cast it back to the original type and access the paramters
c := r.(*Config)
fmt.Println("id", c.ID) // = myapp_81, where 81 is a random number between 0 and 100
fmt.Println("db_connection_string", c.db_connection_string) // = postgresql://admin:admin@localhost:5432/mydatabase

Struct Tags

To create types that can be converted from HCL your top level resource needs to embed the following type into your structs.

types.ResourceBase `hcl:",remain"`

The struct tag `hcl:",remain"`, must be included with this type as it tells the HCL parser to unfold the default properties such as disabled and depends_on from your custom type.

Basic Attributes

If you add the field Location string `hcl:"location"` to your type this will mean that the hcl attribute location will be parsed into this Field. This creates a required attribute for HCL, not providing the location attribute on the hcl representing the PostgresSQL struct will result in a parser error.

type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location"`
}
Optional Attributes

To create optional attribute you can add the optional keyword to the struct tag the previous example has been modified to make location optional.

type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location,optional"`
}
Mandatory Blocks

To define child blocks in your configuration you can specify a field that contains another struct. In the following example the Timeouts field specifies that the Config must be specified with a mandatory child stanza timeouts.

To configure a block the block struct tag is used after the hcl attribute name.

`hcl:"timeouts,block"`

This can be seen in the following code sample.

type Config struct {
	types.ResourceBase `hcl:",remain"`

	DBConnectionString string `hcl:"db_connection_string"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts Timeouts `hcl:"timeouts,block"`
}

This would be configured using the following HCL.

resource "config" "myconfig" {
  db_connection_string = "abc"
  timeouts {
    tls_handshake = 10
  }
}

The Timeout type used by the field Timeout does not need to embed ResourceBase as it is not a top level resource but all other struct tags that define blocks and optional parameters are required.

Optional Blocks

To make child blocks optional you simply need to change the Field type to a reference

type Config struct {
	types.ResourceBase `hcl:",remain"`

	DBConnectionString string `hcl:"db_connection_string"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts *Timeouts `hcl:"timeouts,block"`
}

timeouts is now optional and will not result in a parser error if not specified.

resource "config" "myconfig" {
  db_connection_string = "abc"
}
Multiple Blocks

To allow a block to be used 0 or more times you can define the Field as a slice.

type Config struct {
	types.ResourceBase `hcl:",remain"`

	DBConnectionString string `hcl:"db_connection_string"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts []Timeouts `hcl:"timeouts,block"`
}

timeouts can now be specified multiple times

resource "config" "myconfig" {
  db_connection_string = "abc"
  
  timeouts {
    tls_handshake = 10
  }
  
  timeouts {
    tls_handshake = 10
  }
}

Note: when parsing the configuration the order of the Timeouts field will correspond to the order of the timeouts blocks as defined in the config.

References to other resources

A resource can reference other resources that can be set through interpolation.

The following structs define a config resource, and a postgres_sql resource.

type Config struct {
	types.ResourceBase `hcl:",remain"`

  // Other structs can be referenced by defining the type
  // to the other struct, the referenced type must implemented types.ResourceBase
	MainDBConnection PostgreSQL `hcl:"main_db_connection"`
  
  // It is also possible to reference arrays of structs 
	OtherDBConnections []PostgreSQL `hcl:"other_db_connections"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts []Timeouts `hcl:"timeouts,block"`
}

type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location,optional"`
}

These are represented as HCL using the following syntax, note: rather than referencing an individual attribute from the postgres_sql resource the entire struct is referenced. When the parser processes the config resource and the references are resolved the value of the referenced resources are copied to the config.

resource "postgres_sql" "main" {
  location = "main.mydomain.com"
}

resource "postgres_sql" "other_1" {
  location = "1.mydomain.com"
}

resource "postgres_sql" "other_2" {
  location = "2.mydomain.com"
}

resource "config" "default" {
  main_db_connection = resource.postgres_sql.main
  other_db_connections = [
    resource.postgres_sql.other_1
    resource.postgres_sql.other_2
  ]
}

You could then access the properties of the referenced PostgreSQL structs in the normal go way.

  r,err := c.FindResource("resource.config.default") 
  if err != nil {
    return err
  }

  conf := r.(*Config)

  fmt.Println("loc main", conf.MainDBConnection.Location)
  fmt.Println("loc other 1", conf.OtherDBConnections[0].Location)
  fmt.Println("loc other 2", conf.OtherDBConnections[1].Location)
Defining shared fields for resources

It is common that you might have two resources that are similar but have some differences. For example, you might have two database, postgres and mysql that share some common fields like location and port but have some differences that are specific to the implementation.

To enable code reuse you can define a shared struct that contains the common fields and then embed this struct into the postgres and mysql structs.

To enable this you define a common type that embed the ResourceBase type and then you can embed this type into the postgres and mysql types. Note: you must use the hcl:",remain" tag to ensure that the fields from the shared type.

type DB struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location,optional"`
	Port     int    `hcl:"port,optional"`
}

type PostgreSQL struct {
  DB `hcl:",remain"`

  MaxLocks int `hcl:"max_locks"`
}

type MySQL struct {
  DB `hcl:",remain"`

  CacheSize int `hcl:"cache_size"`
}

Variables

Variables allow dynamic values to be set in your configuration, they are defined using the variable resource stanza.

variable "username" {
  default = "root"
}

variable "connection_string" {
  default = "root:password@localhost"
}

Setting a default value for a variable will enable it to be used within resources.

resource "config" "myconfig1" {
  db_connection_string = variable.connection_string 
}

resource "config" "myconfig2" {
  db_connection_string = "${variable.username}:password@localhost"
}

Variables can also be overridden by setting the corresponding environment variable. For example to set the variable username, you prefix the environment variable with HCL_VAR_, so to set username you could do set the following:

export HCL_VAR_username="nic"

The prefix for environment variables can be changed in the ParserOptions.

Note: variables can contain interpolated references for other resources as the are not parsed by the graph and are parsed before any other resource.

For computed local variables use local resources.

Local

Local resources allow you to create, temporary computed variables that can be used within your config. For example, if you wanted to compute a value that was based on the attribute of another resource you could use a local.

resource "config" "myconfig1" {
  db_connection_string = variable.connection_string 
}

local "conn" {
  value = resource.config.myconfig1.db_connection_string == "abc" ? "localhost" : resource.config.myconfig1.db_connection_string
}

resource "config" "myconfig2" {
  db_connection_string = local.conn
}

Unlike variables local variables are part of the graph and can contain references to other resources.

Modules

HCLConfig supports modular configuration that enables you to group your configuration or encapsulate certain functionality into modules.

A module is a default type, however you still need to create the go structs that define the resources included in your module. The following example shows how you can use the module that is defined in ./example/modules/db/db.hcl

Any sub folder can be a module, to create a module all that is needed is one or more .hcl files that contain your custom resources.

// modules can also use 
module "mymodule_1" {
  source = "../example/modules/db"

  variables = {
    db_username = "root"
    db_password = "password"
  }
}

Modules can also be imported from remote sources such as a GitHub repository, to version a module the SHA of the commit can be used.

module "mymodule_1" {
  source = "github.com/jumppad-labs/hclconfig?ref=9173050/example/modules//db"

  variables = {
    db_username = variable.db_username
    db_password = "topsecret"
  }
}
Inputs

To enable dynamic module use, variables and outputs can be used to define the interface for your module. Variables can be define inside the module and the value set explicitly using the variables block as shown in the previous example.

Outputs

To return a value from a module you can define an output, the db module defines the output connection_string.

output "connection_string" {
  value = resource.postgres.mydb.connection_string
}

To read this value you can use the interpolation syntax module.mymodule_1.output.name The following example shows how an output from one module can be used as an input to another module. Because HCLConfig understands the links between resources the resources in my_other_module will only be processed after the resources in mymodule_1.

module "mymodule_1" {
  source = "../example/modules/db"

  variables = {
    db_username = "root"
    db_password = "password"
  }
}

module "my_other_module" {
  source = "../example/modules/app"

  variables = {
    db_connection_string = module.mymodule_1.output.connection_string
  }
}

Outputs can also contain complex types like lists ...

output "connection_string_list" {
  value = [
    resource.postgres.mydb1.connection_string,
    resource.postgres.mydb2.connection_string
  ]
}

and maps ...

output "connection_string_map" {
  value = {
    connection1 = resource.postgres.mydb1.connection_string
    connection2 = resource.postgres.mydb2.connection_string
  }
}

It is possible to consume these values like so ...

output "connection_string_list_1" {
  value = output.connection_string_list.0
}

output "connection_string_map_1" {
  value = output.connection_string_map.connection1
}

Functions

HCLConfig supports functions that can be used inside your configuration

postgres "mydb" {
  location = "localhost"
  port = 5432
  name = "mydatabase"

  username = var.db_username

  // functions can be used inside the configuration,
  // functions are evaluated when the configuration is parsed 
  password = env("DB_PASSWORD")
}
Default functions

For convenience HCLConfig has the following default functions:

len(type)

Returns the length of a string or collection

mytype "test" {
  collection = ["one", "two"]
  string = "mystring"
}

myothertype "test" {
  // Value = 2
  collection_length = len(resource.mytype.test.collection)

  // Value = 8
  string_length = len(resource.mytype.test.string)
}
env(name)

Returns the value of a system environment variable

mytype "test" {
  // returns the value of the system environment variable $GOPATH
  gopath = env("GOPATH")
}
home()

Returns the location of the users home directory

mytype "test" {
  // returns the value of the system home directory
  home_folder = home()
}
file(path)

Returns the contents of a file at the given path.


# given the file "./myfile.txt" with the contents "foo bar"

mytype "test" {
  // my_file = "foobar"
  my_file = file("./myfile.txt")
}
template_file(path, variables)

Returns the rendered contents of a template file at the given path with the given input variables.

Templates can leverage the Handlebars templating language, more details on Handlebars can be found at the following link:

https://handlebarsjs.com/

#given a file "./mytemplate.tmpl" with the contents "hello {{name}}"

mytype "test" {
  // my_file = "foobar"
  my_file = template_file("./mytemplate.tmpl", {
    name = "world"
  })
}
Template Helpers

The template_file function provides helpers that can be used inside your templates as shown in the example below.

resource "template" "consul_config" {

  source = <<-EOF

  file_content = "{{ file "./myfile.txt" }}"
  quote = {{quote something}} 
  trim = {{quote (trim with_whitespace)}}

  EOF

  destination = "./consul_config/consul.hcl"
}
quote [string]

Returns the original string wrapped in quotations, quote can be used with the Go template pipe modifier.

// given the string abc

quote "abc" // would return the value "abc"
trim [string]

Removes whitespace such as carrige returns and spaces from the begining and the end of the string, can be used with the Go template pipe modifier.

// given the string abc

trim " abc " // would return the value "abc"
dir()

Returns the absolute path of the directory containing the current resource

mytype "test" {
  resource_folder = dir()
}
trim(string)

Returns the given string with leading and trailing whitespace removed of the given string

mytype "test" {
  // trimmed = "abc 123"
  trimmed = trim("  abc  123   ")
}
element(list | map, int | string)

Returns a value from a map or list by the given index.

variable "property" {
  default = "name"
}

mytype "test1" {
  // trimmed = "abc 123"
  item {
    name = "nic"
  }
  
  item {
    name = "eric"
  }
}

mytype "test2" {
  item {
    name = element(resource.mytype.test1.0, variable property)
  }
}
Custom Functions

In addition to the default functions it is possible to register custom functions.

For example, given a requirement to have a function that returns a random number in a set range you could write a go function that looks like the following. Note: only a single return type can be consumed by the HCL parser and assigned to the resource value.

func RandRange(min, max int) int {
	return rand.Intn((max-min)+1) + min
}

This could then be referenced in the following config

postgres "mydb" {
  location = "localhost"

  // custom function to return a random number between 5000 and 6000
  port = rand(5000,6000)
  
  name = "mydatabase"
}

You set up the parser as normal

p := NewParser(DefaultOptions())
p.RegisterType("postgres", &structs.Postgres{})

However, in order to use the custom function before parsing you register it with the RegisterFunction method as shown below.

p.RegisterFunction("rand", RandRange)

At present only the following simple types are supported for custom functions

  • string
  • uint
  • uint32
  • uint64
  • int
  • int32
  • int64
  • float32
  • float64
Errors in custom functions

To signify that an error occurred in a custom function and to halt parsing of the config your function can optionally return a tuple of (type, error). For example to add error handling to the random function you could write it as shown below.

func RandRange(min, max int) (int, error) {
  if min >= max {
    return -1, fmt.Errorf("minimum value '%d' must be smaller than the maximum value '%d')
  }

	return rand.Intn((max-min)+1) + min
}

Lifecycle Callbacks

HCLConfig provides three hooks that can be used when parsing configuration.

  • Resource Processable interface
  • Parser Callback
  • Config Process Callback
Resource Processable interface

The resource Processable interface can be added to your resources by adding a an optional method with the following singature.

Process() error

For example, the PostgresSQL resource implement the Processable interface to compute the value of the attribute connection_string.

type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location"`
	Port     int    `hcl:"port"`
	DBName   string `hcl:"name"`
	Username string `hcl:"username"`
	Password string `hcl:"password"`

	// ConnectionString is a computed field and must be marked optional
	ConnectionString string `hcl:"connection_string,optional"`
}

// Process is called using an order calculated from the dependency graph
// this is where you can set any computed fields
func (t *PostgreSQL) Process() error {
	t.ConnectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", t.Username, t.Password, t.Location, t.Port, t.DBName)
	return nil
}

Process is called in strict order depending on the dependencies for your resources.

For example, given the following custom resources

// Config defines the type `config`
type Config struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	ID string `hcl:"id"`

	DBConnectionString string `hcl:"db_connection_string"`

	// Fields that are of `struct` type must be marked using the `block`
	// parameter in the tags. To make a `block` Field, types marked as block must be
	// a reference i.e. *Timeouts
	Timeouts *Timeouts `hcl:"timeouts,block"`
}

func (t *Config) Process() error {
	// override default values
	if t.Timeouts.TLSHandshake == 0 {
		t.Timeouts.TLSHandshake = 5
	}

	return nil
}

// PostgreSQL defines the Resource `postgres`
type PostgreSQL struct {
	// For a resource to be parsed by HCLConfig it needs to embed the ResourceInfo type and
	// add the methods from the `Resource` interface
	types.ResourceBase `hcl:",remain"`

	Location string `hcl:"location"`
	Port     int    `hcl:"port"`
	DBName   string `hcl:"name"`
	Username string `hcl:"username"`
	Password string `hcl:"password"`

	// ConnectionString is a computed field and must be marked optional
	ConnectionString string `hcl:"connection_string,optional"`
}

// Process is called using an order calculated from the dependency graph
// this is where you can set any computed fields
func (t *PostgreSQL) Process() error {
	t.ConnectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", t.Username, t.Password, t.Location, t.Port, t.DBName)
	return nil
}

And the following configuration that uses these resources

resource "config" "myconfig" {
  // resource.postgres.mydb.connection_string will be available after the `Process` has
  // been called on the `postgres` resource. HCLConfig understands dependency and will
  // call Process in a strict order
  db_connection_string = resource.postgres.mydb.connection_string
}

resource "postgres" "mydb" {
  location = "localhost"
  port     = 5432
  name     = "mydatabase"

  // Varaibles can be used to set values, the default values for these variables will be overidden
  // by values set by the environment variables HCL_db_username and HCL_db_password
  username = variable.db_username
  password = variable.db_password
}

Because you are referencing the attribute resource.postgres.mydb.connection_string to set a value in the config resource. Process for the PostgreSQL type will be called before Process for the Config type. This allows you to perform any computations or validations needed to calculate connection_string before config attempts to consume the value.

Returning an error from Process will immediately exit the ParseFile or ParseDirectory method.

Parser Callback

Rather than implementing individual resource functions you may prefer to leverage the global callback that can be set on the ParserOptions.

o := hclconfig.DefaultOptions()

// set the callback that will be executed when a resource has been created
// this function can be used to execute any external work required for the
// resource.
o.ParseCallback = func(r types.Resource) error {
	fmt.Printf(
    "resource '%s' named '%s' has been parsed from the file: %s\n", 
    r.Metadata().Type, 
    r.Metadata().Name, 
    r.Metadata().File,
  )

  // cast the Resource into a concrete type
  switch r.Metadata().Type {
    case "config":
      myconfig := r.(*Config)
      fmt.Println(myconfig.DBConnectionString)
  }

	return nil
}

The ParseCallback function is executed after the Processable interface and respects the same call order that is implemented for Processable.

Config Process function

A final callback is available using the Process(wf ProcessCallback, reverse bool) error function that is available on the hclconfig.Config type.

Process builds a Directed Acyclic Graph for your configuration based on the dependency and calls the provided ProcessCallback for each resource in the graph.

nc, _ := p.ParseFile("./config.hcl")

nc.Process(func(r types.Resource) error {
	fmt.Println("  ", r.Metadata().ID)
	return nil
}, false)

Note
While you can mutate the values of the Resource passed to the ProcessCallback, it will not update any resources that reference this attribute.

When ParseFile resolves interpolated values it copies the value to the destination resource. Given the earlier example mutating the ConnectionString field on the postgres resource would not update the config resource even though the ProcessCallback will be called with the PostgreSQL type before Config.

Walking dependencies in reverse

To reverse the order of resources that are provided to the ProcessCallback you can set the second process method attribute to true.

nc, _ := p.ParseFile("./config.hcl")

nc.Process(func(r types.Resource) error {
	fmt.Println("  ", r.Metadata().ID)
	return nil
}, true)

ProcessCallback will be called first for resources lowest down in the dependency graph children before the resources they depend on.

An ideal use for this method is to clean up any operations that may have been created with the Processable interface on your resource or the ParseCallback.

Serialization

To save state the hclconfig.Config type can be serialized to JSON using the following method.

d, err := c.ToJSON()
ioutil.WriteFile("./config.json", d, os.ModePerm)

Deserialization

To deserialize hclconfig.Config that has been serialized with the ToJSON method you can use the UnmarshalJSON method on the Parser.

UnmarshalJSON will reconstruct the concrete types based on the configured resources.

d, _ := ioutil.ReadFile("./config.json")
nc, err := p.UnmarshalJSON(d)
if err != nil {
	fmt.Printf("An error occurred unmarshalling the config: %s\n", err)
	os.Exit(1)
}

Documentation

Index

Constants

View Source
const LineEnding = "\n"

Variables

This section is empty.

Functions

func HashString added in v0.8.0

func HashString(in string) string

HashString creates an MD5 hash of the given string

func ParseVars

func ParseVars(value map[string]cty.Value) map[string]interface{}

ParseVars converts a map[string]cty.Value into map[string]interface where the interface are generic go types like string, number, bool, slice, map

func ReadFileLocation added in v0.8.0

func ReadFileLocation(filename string, startLine, startCol, endLine, endCol int) (string, error)

ReadFileLocation reads a file between the given locations

Types

type Config

type Config struct {
	Resources []types.Resource `json:"resources"`
	// contains filtered or unexported fields
}

Config defines the stack config

func NewConfig

func NewConfig() *Config

New creates a new Config

func (*Config) AppendResource

func (c *Config) AppendResource(r types.Resource) error

AppendResource adds a given resource to the resource list if the resource already exists an error will be returned

func (*Config) AppendResourcesFromConfig

func (c *Config) AppendResourcesFromConfig(new *Config) error

AppendResourcesFromConfig adds the resources in the given config to this config. If a resources all ready exists a ResourceExistsError error is returned

func (*Config) Diff added in v0.16.0

func (c *Config) Diff(o *Config) (*ResourceDiff, error)

Diff compares the current configuration to the provided configuration and returns resources that have changed between the two configurations

func (*Config) FindModuleResources

func (c *Config) FindModuleResources(module string, includeSubModules bool) ([]types.Resource, error)

FindModuleResources returns an array of resources for the given module if includeSubModules is true then resources in any submodules are also returned if includeSubModules is false only the resources defined in the given module are returned

func (*Config) FindRelativeResource

func (c *Config) FindRelativeResource(path string, parentModule string) (types.Resource, error)

func (*Config) FindResource

func (c *Config) FindResource(path string) (types.Resource, error)

FindResource returns the resource for the given name name is defined with the convention: resource.[type].[name] the keyword "resource" is a required component in the path to allow names of resources to contain "." and to enable easy separate of module parts.

"module" is an optional path parameter: module.[module_name].resource.[type].[name] and is required when searching for resources that have the Module flag set.

If a resource can not be found, resource will be null and an error will be returned

e.g. to find a cluster named k3s r, err := c.FindResource("resource.cluster.k3s")

e.g. to find a cluster named k3s in the module module1 r, err := c.FindResource("module.module1.resource.cluster.k3s")

func (*Config) FindResourcesByType

func (c *Config) FindResourcesByType(t string) ([]types.Resource, error)

FindResourcesByType returns the resources from the given type

func (*Config) RemoveResource

func (c *Config) RemoveResource(rf types.Resource) error

func (*Config) ResourceCount

func (c *Config) ResourceCount() int

ResourceCount defines the number of resources in a config

func (*Config) ToJSON

func (c *Config) ToJSON() ([]byte, error)

ToJSON converts the config to a serializable json string to unmarshal the output of this method back into a config you can use the Parser.UnmarshalJSON method

func (*Config) Walk added in v0.16.0

func (c *Config) Walk(wf WalkCallback, reverse bool) error

Walk creates a Directed Acyclic Graph for the configuration resources depending on their links and references. All the resources defined in the graph are traversed and the provided callback is executed for every resource in the graph.

Any error returned from the ProcessCallback function halts execution of any other callback for resources in the graph.

Specifying the reverse option to 'true' causes the graph to be traversed in reverse order.

type Getter

type Getter interface {
	// Get fetches the source files from src and downloads them to the
	// given folder. If the files already exist at the given location
	// Get does nothing unless ignoreCache is true when source will be
	// downloaded regarless of cache.
	//
	// Get returns a string with the full path of the downloaded source
	// this contains any url characters in src correctly encoded for
	// a filepath.
	Get(src, destFolder string, ignoreCache bool) (string, error)
}

func NewGoGetter

func NewGoGetter() Getter

type GoGetter

type GoGetter struct {
	// contains filtered or unexported fields
}

func (*GoGetter) Get

func (g *GoGetter) Get(src, dest string, ignoreCache bool) (string, error)

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser can parse HCL configuration files

func NewParser

func NewParser(options *ParserOptions) *Parser

NewParser creates a new parser with the given options if options are nil, default options are used

func (*Parser) ParseDirectory

func (p *Parser) ParseDirectory(dir string) (*Config, error)

ParseDirectory parses all resource and variable files in the given directory note: this method does not recurse into sub folders error can be cast to *ConfigError to get a list of errors

func (*Parser) ParseFile

func (p *Parser) ParseFile(file string) (*Config, error)

ParseDirectory parses all resources in the given file error can be cast to *ConfigError to get a list of errors

func (*Parser) RegisterFunction

func (p *Parser) RegisterFunction(name string, f interface{}) error

RegisterFunction type registers a custom interpolation function with the given name the parser uses this list to convert hcl defined resources into concrete types

func (*Parser) RegisterType

func (p *Parser) RegisterType(name string, resource types.Resource)

RegisterType type registers a struct that implements Resource with the given name the parser uses this list to convert hcl defined resources into concrete types

func (*Parser) UnmarshalJSON

func (p *Parser) UnmarshalJSON(d []byte) (*Config, error)

UnmarshalJSON parses a JSON string from a serialized Config and returns a valid Config.

type ParserOptions

type ParserOptions struct {
	// list of default variable values to add to the parser
	Variables map[string]string
	// list of variable files to be read by the parser
	VariablesFiles []string
	// environment variable prefix
	VariableEnvPrefix string
	// location of any downloaded modules
	ModuleCache string
	// Callback executed when the parser reads a resource stanza, callbacks are
	// executed based on a directed acyclic graph. If resource 'a' references
	// a property defined in resource 'b', i.e 'resource.a.myproperty' then the
	// callback for resource 'b' will be executed before resource 'a'. This allows
	// you to set the dependent properties of resource 'b' before resource 'a'
	// consumes them.
	Callback WalkCallback
}

func DefaultOptions

func DefaultOptions() *ParserOptions

DefaultOptions returns a ParserOptions object with the ModuleCache set to the default directory of $HOME/.hclconfig/cache if the $HOME folder can not be determined, the cache is set to the current folder VariableEnvPrefix is set to 'HCL_VAR_', should a variable be defined called 'foo' setting the environment variable 'HCL_VAR_foo' will override any default value

type ResourceDiff added in v0.16.0

type ResourceDiff struct {
	// Resources that have been added to the configuration
	Added []types.Resource
	// Resources that have been updated after the parse step, typically this is
	// any change to the resource definition, but does not include changes to referenced
	// resources
	// It is possible that a resource is in both ParseUpdated and ProcessUpdated
	ParseUpdated []types.Resource
	// Resources that have been updated after the process step, typically this includes
	// any changes to referenced resources
	// It is possible that a resource is in both ParseUpdated and ProcessUpdated
	ProcessedUpdated []types.Resource
	// Resources that have been removed from the configuration
	Removed []types.Resource
	// Resources that have not changed
	Unchanged []types.Resource
}

ResourceDiff is a container for resources that have changed between two different configurations

type ResourceExistsError

type ResourceExistsError struct {
	Name string
}

ResourceExistsError is thrown when a resource already exists in the resource list

func (ResourceExistsError) Error

func (e ResourceExistsError) Error() string

type ResourceNotFoundError

type ResourceNotFoundError struct {
	Name string
}

ResourceNotFoundError is thrown when a resource could not be found

func (ResourceNotFoundError) Error

func (e ResourceNotFoundError) Error() string

type ResourceTypeNotExistError

type ResourceTypeNotExistError struct {
	Type string
	File string
}

func (ResourceTypeNotExistError) Error

type WalkCallback added in v0.16.0

type WalkCallback func(r types.Resource) error

WalkCallback is called with the resource when the graph processes that particular node

Directories

Path Synopsis
Small library on top of reflect for make lookups to Structs or Maps.
Small library on top of reflect for make lookups to Structs or Maps.
Example showing how it is possible to generate dynamic structs from a schema This could be used to move HCL config away from concrete to dynamic types
Example showing how it is possible to generate dynamic structs from a schema This could be used to move HCL config away from concrete to dynamic types
test_fixtures

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL