diff --git a/.web-docs/components/builder/linode/README.md b/.web-docs/components/builder/linode/README.md index c8915c16..3ce0501b 100644 --- a/.web-docs/components/builder/linode/README.md +++ b/.web-docs/components/builder/linode/README.md @@ -45,6 +45,9 @@ can also be supplied to override the typical auto-generated key: `images:read_write`, `linodes:read_write`, and `events:read_only` scopes are required for the API token. +- `api_ca_path` (string) - The path to a CA file to trust when making API requests. + It can also be specified using the `LINODE_CA` environment variable. + diff --git a/.web-docs/components/data-source/image/README.md b/.web-docs/components/data-source/image/README.md index 125f85bb..8d0cf016 100644 --- a/.web-docs/components/data-source/image/README.md +++ b/.web-docs/components/data-source/image/README.md @@ -69,6 +69,9 @@ data "linode-image" "ubuntu22_lts" { `images:read_write`, `linodes:read_write`, and `events:read_only` scopes are required for the API token. +- `api_ca_path` (string) - The path to a CA file to trust when making API requests. + It can also be specified using the `LINODE_CA` environment variable. + diff --git a/builder/linode/builder.go b/builder/linode/builder.go index 390ab313..90d38c7a 100644 --- a/builder/linode/builder.go +++ b/builder/linode/builder.go @@ -39,8 +39,16 @@ func (b *Builder) Prepare(raws ...any) ([]string, []string, error) { func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (ret packersdk.Artifact, err error) { ui.Say("Running builder ...") + var client *linodego.Client - client := helper.NewLinodeClient(b.config.PersonalAccessToken) + if b.config.APICAPath != "" { + client, err = helper.NewLinodeClientWithCA(b.config.PersonalAccessToken, b.config.APICAPath) + if err != nil { + return nil, err + } + } else { + client = helper.NewLinodeClient(b.config.PersonalAccessToken) + } state := new(multistep.BasicStateBag) state.Put("config", &b.config) @@ -90,7 +98,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) artifact := Artifact{ ImageLabel: image.Label, ImageID: image.ID, - Driver: &client, + Driver: client, StateData: map[string]any{ "generated_data": state.Get("generated_data"), "source_image": b.config.Image, diff --git a/builder/linode/config.go b/builder/linode/config.go index e9abcc31..f1ab4e0f 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -186,6 +186,10 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { c.PersonalAccessToken = os.Getenv("LINODE_TOKEN") } + if c.APICAPath == "" { + c.APICAPath = os.Getenv("LINODE_CA") + } + if c.ImageLabel == "" { if def, err := interpolate.Render("packer-{{timestamp}}", nil); err == nil { c.ImageLabel = def diff --git a/builder/linode/config.hcl2spec.go b/builder/linode/config.hcl2spec.go index 63a97488..98093c6f 100644 --- a/builder/linode/config.hcl2spec.go +++ b/builder/linode/config.hcl2spec.go @@ -19,6 +19,7 @@ type FlatConfig struct { PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` PersonalAccessToken *string `mapstructure:"linode_token" cty:"linode_token" hcl:"linode_token"` + APICAPath *string `mapstructure:"api_ca_path" cty:"api_ca_path" hcl:"api_ca_path"` Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"` @@ -111,6 +112,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, "linode_token": &hcldec.AttrSpec{Name: "linode_token", Type: cty.String, Required: false}, + "api_ca_path": &hcldec.AttrSpec{Name: "api_ca_path", Type: cty.String, Required: false}, "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, diff --git a/builder/linode/step_create_image.go b/builder/linode/step_create_image.go index 003ee892..ed7a2b7f 100644 --- a/builder/linode/step_create_image.go +++ b/builder/linode/step_create_image.go @@ -10,7 +10,7 @@ import ( ) type stepCreateImage struct { - client linodego.Client + client *linodego.Client } func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { diff --git a/builder/linode/step_create_linode.go b/builder/linode/step_create_linode.go index 525dbdab..44deff4d 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -11,7 +11,7 @@ import ( ) type stepCreateLinode struct { - client linodego.Client + client *linodego.Client } func flattenConfigInterfaceIPv4(i *InterfaceIPv4) *linodego.VPCIPv4 { diff --git a/builder/linode/step_shutdown_linode.go b/builder/linode/step_shutdown_linode.go index 3ad2f498..a2b47f3a 100644 --- a/builder/linode/step_shutdown_linode.go +++ b/builder/linode/step_shutdown_linode.go @@ -10,7 +10,7 @@ import ( ) type stepShutdownLinode struct { - client linodego.Client + client *linodego.Client } func (s *stepShutdownLinode) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { diff --git a/datasource/image/data.go b/datasource/image/data.go index 06dd5057..b106abfd 100644 --- a/datasource/image/data.go +++ b/datasource/image/data.go @@ -129,7 +129,17 @@ func (d *Datasource) OutputSpec() hcldec.ObjectSpec { } func (d *Datasource) Execute() (cty.Value, error) { - client := helper.NewLinodeClient(d.config.PersonalAccessToken) + var client *linodego.Client + var err error + + if d.config.APICAPath != "" { + client, err = helper.NewLinodeClientWithCA(d.config.PersonalAccessToken, d.config.APICAPath) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + } else { + client = helper.NewLinodeClient(d.config.PersonalAccessToken) + } filters := linodego.Filter{} diff --git a/datasource/image/data.hcl2spec.go b/datasource/image/data.hcl2spec.go index 1b2192d2..76587c03 100644 --- a/datasource/image/data.hcl2spec.go +++ b/datasource/image/data.hcl2spec.go @@ -19,6 +19,7 @@ type FlatConfig struct { PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` PersonalAccessToken *string `mapstructure:"linode_token" cty:"linode_token" hcl:"linode_token"` + APICAPath *string `mapstructure:"api_ca_path" cty:"api_ca_path" hcl:"api_ca_path"` Label *string `mapstructure:"label" cty:"label" hcl:"label"` LabelRegex *string `mapstructure:"label_regex" cty:"label_regex" hcl:"label_regex"` ID *string `mapstructure:"id" cty:"id" hcl:"id"` @@ -47,6 +48,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, "linode_token": &hcldec.AttrSpec{Name: "linode_token", Type: cty.String, Required: false}, + "api_ca_path": &hcldec.AttrSpec{Name: "api_ca_path", Type: cty.String, Required: false}, "label": &hcldec.AttrSpec{Name: "label", Type: cty.String, Required: false}, "label_regex": &hcldec.AttrSpec{Name: "label_regex", Type: cty.String, Required: false}, "id": &hcldec.AttrSpec{Name: "id", Type: cty.String, Required: false}, diff --git a/helper/client.go b/helper/client.go index 429169dd..3b1d11db 100644 --- a/helper/client.go +++ b/helper/client.go @@ -1,8 +1,12 @@ package helper import ( + "crypto/tls" + "crypto/x509" "fmt" "net/http" + "os" + "path/filepath" "github.com/linode/linodego" "github.com/linode/packer-plugin-linode/version" @@ -11,14 +15,31 @@ import ( const TokenEnvVar = "LINODE_TOKEN" -func NewLinodeClient(token string) linodego.Client { - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) +// AddRootCAToTransport applies the CA at the given path to the given *http.Transport +func AddRootCAToTransport(CAPath string, transport *http.Transport) error { + CAData, err := os.ReadFile(filepath.Clean(CAPath)) + if err != nil { + return fmt.Errorf("failed to read CA file %s: %w", CAPath, err) + } - oauthTransport := &oauth2.Transport{ - Source: tokenSource, + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + + if transport.TLSClientConfig.RootCAs == nil { + transport.TLSClientConfig.RootCAs = x509.NewCertPool() } + + transport.TLSClientConfig.RootCAs.AppendCertsFromPEM(CAData) + + return nil +} + +func linodeClientFromTransport(transport http.RoundTripper) *linodego.Client { oauth2Client := &http.Client{ - Transport: oauthTransport, + Transport: transport, } client := linodego.NewClient(oauth2Client) @@ -28,5 +49,33 @@ func NewLinodeClient(token string) linodego.Client { version.PluginVersion.FormattedVersion(), projectURL, linodego.Version) client.SetUserAgent(userAgent) - return client + return &client +} + +func getDefaultTransportWithCA(CAPath string) (*http.Transport, error) { + httpTransport := http.DefaultTransport.(*http.Transport).Clone() + return httpTransport, AddRootCAToTransport(CAPath, httpTransport) +} + +func getOauth2TransportWithToken(token string, baseTransport http.RoundTripper) *oauth2.Transport { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + oauthTransport := &oauth2.Transport{ + Source: tokenSource, + Base: baseTransport, + } + return oauthTransport +} + +func NewLinodeClient(token string) *linodego.Client { + oauthTransport := getOauth2TransportWithToken(token, nil) + return linodeClientFromTransport(oauthTransport) +} + +func NewLinodeClientWithCA(token, CAPath string) (*linodego.Client, error) { + transport, err := getDefaultTransportWithCA(CAPath) + if err != nil { + return nil, err + } + oauthTransport := getOauth2TransportWithToken(token, transport) + return linodeClientFromTransport(oauthTransport), nil } diff --git a/helper/common.go b/helper/common.go index 13dfa3e6..b74b552b 100644 --- a/helper/common.go +++ b/helper/common.go @@ -10,4 +10,8 @@ type LinodeCommon struct { // `images:read_write`, `linodes:read_write`, and `events:read_only` // scopes are required for the API token. PersonalAccessToken string `mapstructure:"linode_token"` + + // The path to a CA file to trust when making API requests. + // It can also be specified using the `LINODE_CA` environment variable. + APICAPath string `mapstructure:"api_ca_path"` }