2025-05-09 10:00:42 -04:00
package provider
import (
"context"
"fmt"
"log"
"strings"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
// import drivers otherwise factories are empty
// for --driver output flag usage
_ "github.com/docker/buildx/driver/docker"
_ "github.com/docker/buildx/driver/docker-container"
_ "github.com/docker/buildx/driver/kubernetes"
_ "github.com/docker/buildx/driver/remote"
)
// resourceDockerBuildxBuilder defines the buildx_builder resource schema
func resourceDockerBuildxBuilder ( ) * schema . Resource {
return & schema . Resource {
CreateContext : resourceDockerBuildxBuilderCreate ,
ReadContext : resourceDockerBuildxBuilderRead ,
DeleteContext : resourceDockerBuildxBuilderDelete ,
Description : "Manages a Docker Buildx builder instance. This resource allows you to create a buildx builder with various configurations such as driver, nodes, and platform settings. Please see https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md for more documentation" ,
Schema : map [ string ] * schema . Schema {
"name" : {
Type : schema . TypeString ,
Default : "" ,
Description : "The name of the Buildx builder. IF not specified, a random name will be generated." ,
ForceNew : true ,
Optional : true ,
} ,
"driver" : {
Type : schema . TypeString ,
Optional : true ,
Default : "docker-container" ,
Description : "The driver to use for the Buildx builder (e.g., docker-container, kubernetes)." ,
ConflictsWith : [ ] string { "docker_container" , "kubernetes" , "remote" } ,
ForceNew : true ,
} ,
"driver_options" : {
Type : schema . TypeMap ,
Optional : true ,
Description : "Additional options for the Buildx driver in the form of `key=value,...`. These options are driver-specific." ,
Elem : & schema . Schema {
Type : schema . TypeString ,
} ,
ConflictsWith : [ ] string { "docker_container" , "kubernetes" , "remote" } ,
ForceNew : true ,
} ,
"node" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Create/modify node with given name" ,
Default : "" ,
ForceNew : true ,
} ,
"platform" : {
Type : schema . TypeList ,
Optional : true ,
Description : "Fixed platforms for current node" ,
Elem : & schema . Schema {
Type : schema . TypeString ,
} ,
ForceNew : true ,
} ,
"buildkit_flags" : {
Type : schema . TypeString ,
Optional : true ,
Description : "BuildKit flags to set for the builder." ,
Default : "" ,
ForceNew : true ,
} ,
"buildkit_config" : {
Type : schema . TypeString ,
Optional : true ,
Description : "BuildKit daemon config file" ,
Default : "" ,
ForceNew : true ,
} ,
"use" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Set the current builder instance as the default for the current context." ,
ForceNew : true ,
} ,
"append" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Append a node to builder instead of changing it" ,
ForceNew : true ,
} ,
"bootstrap" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Automatically boot the builder after creation. Defaults to `false`" ,
ForceNew : true ,
} ,
2025-06-04 13:51:09 -04:00
"endpoint" : {
Type : schema . TypeString ,
Optional : true ,
Description : "The endpoint or context to use for the Buildx builder, where context is the name of a context from docker context ls and endpoint is the address for Docker socket (eg. DOCKER_HOST value). By default, the current Docker configuration is used for determining the context/endpoint value." ,
Default : "" ,
ForceNew : true ,
} ,
2025-05-09 10:00:42 -04:00
"kubernetes" : {
Type : schema . TypeList ,
Optional : true ,
MaxItems : 1 ,
Description : "Configuration block for the Kubernetes driver." ,
ConflictsWith : [ ] string { "docker_container" , "remote" , "driver" , "driver_options" } ,
ForceNew : true ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"image" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the image to use for running BuildKit." ,
} ,
"namespace" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the Kubernetes namespace." ,
} ,
"default_load" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Automatically load images to the Docker Engine image store. Defaults to `false`" ,
} ,
"replicas" : {
Type : schema . TypeInt ,
Optional : true ,
Default : 1 ,
Description : "Sets the number of Pod replicas to create." ,
} ,
"requests" : {
Type : schema . TypeList ,
MaxItems : 1 ,
Optional : true ,
Description : "Resource requests for CPU, memory, and ephemeral storage." ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"cpu" : {
Type : schema . TypeString ,
Optional : true ,
Description : "CPU limit for the Kubernetes pod." ,
} ,
"memory" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Memory limit for the Kubernetes pod." ,
} ,
"ephemeral_storage" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Ephemeral storage limit for the Kubernetes pod." ,
} ,
} ,
} ,
} ,
"limits" : {
Type : schema . TypeList ,
Optional : true ,
MaxItems : 1 ,
Description : "Resource limits for CPU, memory, and ephemeral storage." ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"cpu" : {
Type : schema . TypeString ,
Optional : true ,
Description : "CPU limit for the Kubernetes pod." ,
} ,
"memory" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Memory limit for the Kubernetes pod." ,
} ,
"ephemeral_storage" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Ephemeral storage limit for the Kubernetes pod." ,
} ,
} ,
} ,
} ,
"nodeselector" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the pod's nodeSelector label(s)." ,
} ,
"annotations" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets additional annotations on the deployments and pods." ,
} ,
"labels" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets additional labels on the deployments and pods." ,
} ,
"tolerations" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Configures the pod's taint toleration." ,
} ,
"serviceaccount" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the pod's serviceAccountName." ,
} ,
"schedulername" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the scheduler responsible for scheduling the pod." ,
} ,
"timeout" : {
Type : schema . TypeString ,
Optional : true ,
Default : "120s" ,
Description : "Set the timeout limit for pod provisioning." ,
} ,
"rootless" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Run the container as a non-root user." ,
} ,
"loadbalance" : {
Type : schema . TypeString ,
Optional : true ,
Default : "sticky" ,
Description : "Load-balancing strategy (sticky or random)." ,
} ,
"qemu" : {
Type : schema . TypeList ,
Optional : true ,
MaxItems : 1 ,
Description : "QEMU emulation configuration." ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"install" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Install QEMU emulation for multi-platform support." ,
} ,
"image" : {
Type : schema . TypeString ,
Optional : true ,
Default : "tonistiigi/binfmt:latest" ,
Description : "Sets the QEMU emulation image." ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
"docker_container" : {
Type : schema . TypeList ,
Optional : true ,
ForceNew : true ,
MaxItems : 1 ,
Description : "Configuration block for the Docker-Container driver." ,
ConflictsWith : [ ] string { "kubernetes" , "remote" , "driver" , "driver_options" } ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"image" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the BuildKit image to use for the container." ,
} ,
"memory" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the amount of memory the container can use." ,
} ,
"memory_swap" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the memory swap limit for the container." ,
} ,
"cpu_quota" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Imposes a CPU CFS quota on the container." ,
} ,
"cpu_period" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the CPU CFS scheduler period for the container." ,
} ,
"cpu_shares" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Configures CPU shares (relative weight) of the container." ,
} ,
"cpuset_cpus" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Limits the set of CPU cores the container can use." ,
} ,
"cpuset_mems" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Limits the set of CPU memory nodes the container can use." ,
} ,
"default_load" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Automatically load images to the Docker Engine image store. Defaults to `false`" ,
} ,
"network" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the network mode for the container." ,
} ,
"cgroup_parent" : {
Type : schema . TypeString ,
Optional : true ,
Default : "/docker/buildx" ,
Description : "Sets the cgroup parent of the container if Docker is using the \"cgroupfs\" driver." ,
} ,
"restart_policy" : {
Type : schema . TypeString ,
Optional : true ,
Default : "unless-stopped" ,
Description : "Sets the container's restart policy." ,
} ,
"env" : {
Type : schema . TypeMap ,
Optional : true ,
Description : "Sets environment variables in the container." ,
Elem : & schema . Schema {
Type : schema . TypeString ,
} ,
} ,
} ,
} ,
} ,
"remote" : {
Type : schema . TypeList ,
Optional : true ,
ForceNew : true ,
MaxItems : 1 ,
Description : "Configuration block for the Remote driver." ,
ConflictsWith : [ ] string { "kubernetes" , "docker_container" , "driver" , "driver_options" } ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
"key" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Sets the TLS client key." ,
} ,
"cert" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Absolute path to the TLS client certificate to present to buildkitd." ,
} ,
"cacert" : {
Type : schema . TypeString ,
Optional : true ,
Description : "Absolute path to the TLS certificate authority used for validation." ,
} ,
"servername" : {
Type : schema . TypeString ,
Optional : true ,
Description : "TLS server name used in requests." ,
} ,
"default_load" : {
Type : schema . TypeBool ,
Optional : true ,
Default : false ,
Description : "Automatically load images to the Docker Engine image store. Defaults to `false`" ,
} ,
} ,
} ,
} ,
} ,
}
}
// resourceDockerBuildxBuilderCreate handles the creation of a Buildx builder
func resourceDockerBuildxBuilderCreate ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
name := d . Get ( "name" ) . ( string )
driver := d . Get ( "driver" ) . ( string )
platform := d . Get ( "platform" ) . ( [ ] interface { } )
driverOptions := make ( [ ] string , 0 )
v , ok := d . GetOk ( "driver_options" )
if ok {
driverOptions = processDriverOptions ( v . ( map [ string ] interface { } ) )
}
appendAction := d . Get ( "append" ) . ( bool )
use := d . Get ( "use" ) . ( bool )
log . Printf ( "[DEBUG] Creating Buildx builder: %s" , name )
if kubernetesConfig , ok := d . GetOk ( "kubernetes" ) ; ok {
driver = "kubernetes"
kubernetes := kubernetesConfig . ( [ ] interface { } ) [ 0 ] . ( map [ string ] interface { } )
driverOptions = processDriverOptions ( kubernetes )
}
if dockerContainerConfig , ok := d . GetOk ( "docker_container" ) ; ok {
driver = "docker-container"
dockerContainer := dockerContainerConfig . ( [ ] interface { } ) [ 0 ] . ( map [ string ] interface { } )
driverOptions = processDriverOptions ( dockerContainer )
}
// Updated the Create function to handle all Remote driver-specific parameters
if remoteConfig , ok := d . GetOk ( "remote" ) ; ok {
driver = "remote"
remote := remoteConfig . ( [ ] interface { } ) [ 0 ] . ( map [ string ] interface { } )
driverOptions = processDriverOptions ( remote )
}
client := meta . ( * ProviderConfig ) . DockerClient
t , error := command . NewDockerCli ( )
if error != nil {
return diag . FromErr ( fmt . Errorf ( "failed to create Docker CLI: %w" , error ) )
}
log . Printf ( "[DEBUG] Docker CLI initialized %#v, %#v" , client , client . DaemonHost ( ) )
err := t . Initialize ( & flags . ClientOptions { Hosts : [ ] string { client . DaemonHost ( ) } } )
if err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to initialize Docker CLI: %w" , err ) )
}
txn , release , err := storeutil . GetStore ( t )
if err != nil {
return diag . FromErr ( err )
}
// Ensure the file lock gets released no matter what happens.
defer release ( )
log . Printf ( "[DEBUG] Creating Buildx builder with name: %s" , name )
log . Printf ( "[DEBUG] Driver: %s" , driver )
log . Printf ( "[DEBUG] Driver options: %s" , driverOptions )
2025-06-04 13:51:09 -04:00
var ep string
v = d . Get ( "endpoint" ) . ( string )
if v != "" {
ep = v . ( string )
}
2025-05-09 10:00:42 -04:00
b , err := builder . Create ( ctx , txn , t , builder . CreateOpts {
Name : name ,
Driver : driver ,
NodeName : d . Get ( "node" ) . ( string ) ,
Platforms : stringListToStringSlice ( platform ) ,
DriverOpts : driverOptions ,
BuildkitdFlags : d . Get ( "buildkit_flags" ) . ( string ) ,
BuildkitdConfigFile : d . Get ( "buildkit_config" ) . ( string ) ,
Use : use ,
2025-06-04 13:51:09 -04:00
Endpoint : ep ,
2025-05-09 10:00:42 -04:00
Append : appendAction ,
} )
if err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to create Buildx builder: %w" , err ) )
}
// The store is no longer used from this point.
// Release it so we aren't holding the file lock during the boot.
release ( )
if d . Get ( "bootstrap" ) . ( bool ) {
if _ , err = b . Boot ( ctx ) ; err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to bootstrap Buildx builder: %w" , err ) )
}
}
d . SetId ( b . Name )
d . Set ( "name" , b . Name )
return resourceDockerBuildxBuilderRead ( ctx , d , meta )
}
func processDriverOptions ( driverOptionsMap map [ string ] interface { } ) [ ] string {
// Iterate over the driver options and append them to a string list
resultStringList := make ( [ ] string , 0 )
for key , value := range driverOptionsMap {
// replace underscores with dashes in the key
key = strings . ReplaceAll ( key , "_" , "-" )
if strValue , ok := value . ( string ) ; ok {
if strValue == "" {
continue
}
resultStringList = append ( resultStringList , fmt . Sprintf ( "%s=%s" , key , strValue ) )
} else if boolValue , ok := value . ( bool ) ; ok && boolValue {
resultStringList = append ( resultStringList , fmt . Sprintf ( "%s=true" , key ) )
}
// handle TypeMap values
if strMap , ok := value . ( map [ string ] interface { } ) ; ok {
resultStringList = append ( resultStringList , processDriverOptions ( strMap ) ... )
}
}
return resultStringList
}
// resourceDockerBuildxBuilderRead handles reading the state of a Buildx builder
// corresponding file in buildx repo: https://github.com/docker/buildx/blob/master/commands/inspect.go
func resourceDockerBuildxBuilderRead ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
client := meta . ( * ProviderConfig ) . DockerClient
dockerCli , error := command . NewDockerCli ( )
if error != nil {
return diag . FromErr ( fmt . Errorf ( "failed to create Docker CLI: %w" , error ) )
}
err := dockerCli . Initialize ( & flags . ClientOptions { Hosts : [ ] string { client . DaemonHost ( ) } } )
if err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to initialize Docker CLI: %w" , err ) )
}
name := d . Id ( )
log . Printf ( "[DEBUG] Reading Buildx builder: %s" , name )
_ , err = builder . New ( dockerCli ,
builder . WithName ( d . Get ( "name" ) . ( string ) ) ,
builder . WithSkippedValidation ( ) ,
)
if err != nil {
2025-09-29 16:08:43 -04:00
log . Printf ( "[DEBUG] Failed to read Buildx builder %s: %v" , name , err )
if strings . Contains ( err . Error ( ) , fmt . Sprintf ( "no builder \"%s\" found" , name ) ) {
log . Printf ( "[DEBUG] Buildx builder %s not found, removing from state" , name )
d . SetId ( "" )
return nil
}
2025-05-09 10:00:42 -04:00
return diag . FromErr ( err )
}
2025-09-29 16:08:43 -04:00
2025-05-09 10:00:42 -04:00
return nil
}
// resourceDockerBuildxBuilderDelete handles the deletion of a Buildx builder
// corresponding file in buildx repo: https://github.com/docker/buildx/blob/master/commands/rm.go
func resourceDockerBuildxBuilderDelete ( ctx context . Context , d * schema . ResourceData , meta interface { } ) diag . Diagnostics {
name := d . Id ( )
log . Printf ( "[DEBUG] Deleting Buildx builder: %s" , name )
client := meta . ( * ProviderConfig ) . DockerClient
dockerCli , err := command . NewDockerCli ( )
if err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to create Docker CLI: %w" , err ) )
}
err = dockerCli . Initialize ( & flags . ClientOptions { Hosts : [ ] string { client . DaemonHost ( ) } } )
if err != nil {
return diag . FromErr ( fmt . Errorf ( "failed to initialize Docker CLI: %w" , err ) )
}
txn , release , err := storeutil . GetStore ( dockerCli )
if err != nil {
return diag . FromErr ( err )
}
defer release ( )
eg , _ := errgroup . WithContext ( ctx )
func ( name string ) {
eg . Go ( func ( ) ( err error ) {
defer func ( ) {
if err == nil {
_ , _ = fmt . Fprintf ( dockerCli . Err ( ) , "%s removed\n" , name )
} else {
_ , _ = fmt . Fprintf ( dockerCli . Err ( ) , "failed to remove %s: %v\n" , name , err )
}
} ( )
b , err := builder . New ( dockerCli ,
builder . WithName ( name ) ,
builder . WithStore ( txn ) ,
builder . WithSkippedValidation ( ) ,
)
if err != nil {
return err
}
nodes , err := b . LoadNodes ( ctx )
if err != nil {
return err
}
if cb := b . ContextName ( ) ; cb != "" {
return errors . Errorf ( "context builder cannot be removed, run `docker context rm %s` to remove this context" , cb )
}
err1 := rm ( ctx , nodes , rmOptions { keepState : false , keepDaemon : false , allInactive : false , force : false } )
if err := txn . Remove ( b . Name ) ; err != nil {
return err
}
if err1 != nil {
return err1
}
return nil
} )
} ( name )
if err := eg . Wait ( ) ; err != nil {
return diag . Errorf ( "failed to remove one or more builders" )
}
return nil
}
func rm ( ctx context . Context , nodes [ ] builder . Node , in rmOptions ) ( err error ) {
for _ , node := range nodes {
if node . Driver == nil {
continue
}
// Do not stop the buildkitd daemon when --keep-daemon is provided
if ! in . keepDaemon {
if err := node . Driver . Stop ( ctx , true ) ; err != nil {
return err
}
}
if err := node . Driver . Rm ( ctx , true , ! in . keepState , ! in . keepDaemon ) ; err != nil {
return err
}
if node . Err != nil {
err = node . Err
}
}
return err
}
type rmOptions struct {
keepState bool
keepDaemon bool
allInactive bool
force bool
}