diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index da42b7e4..444c69ff 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Run Labeler - uses: crazy-max/ghaction-github-labeler@de749cf181958193cb7debf1a9c5bb28922f3e1b + uses: crazy-max/ghaction-github-labeler@b54af0c25861143e7c8813d7cbbf46d2c341680c with: github-token: ${{ secrets.GITHUB_TOKEN }} yaml-file: .github/labels.yml diff --git a/.github/workflows/release-notify-slack.yml b/.github/workflows/release-notify-slack.yml new file mode 100644 index 00000000..a4e95fae --- /dev/null +++ b/.github/workflows/release-notify-slack.yml @@ -0,0 +1,30 @@ +name: Notify Dev DX Channel on Release +on: + release: + types: [published] + workflow_dispatch: null + +jobs: + notify: + if: github.repository == 'linode/packer-plugin-linode' + runs-on: ubuntu-latest + steps: + - name: Notify Slack - Main Message + id: main_message + uses: slackapi/slack-github-action@v1.27.0 + with: + channel-id: ${{ secrets.DEV_DX_SLACK_CHANNEL_ID }} + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*New Release Published: _packer-plugin-linode_ <${{ github.event.release.html_url }}|${{ github.event.release.tag_name }}> is now live!* :tada:" + } + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} \ No newline at end of file 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/go.mod b/go.mod index c750560e..e97f5bf0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.5 require ( github.com/hashicorp/hcl/v2 v2.22.0 github.com/hashicorp/packer-plugin-sdk v0.5.4 - github.com/linode/linodego v1.41.0 + github.com/linode/linodego v1.42.0 github.com/mitchellh/mapstructure v1.5.0 github.com/zclconf/go-cty v1.13.3 golang.org/x/crypto v0.28.0 @@ -84,7 +84,7 @@ require ( go.opencensus.io v0.24.0 // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect diff --git a/go.sum b/go.sum index ae44d096..d35d947a 100644 --- a/go.sum +++ b/go.sum @@ -218,8 +218,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linode/linodego v1.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY= -github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY= +github.com/linode/linodego v1.42.0 h1:ZSbi4MtvwrfB9Y6bknesorvvueBGGilcmh2D5dq76RM= +github.com/linode/linodego v1.42.0/go.mod h1:2yzmY6pegPBDgx2HDllmt0eIk2IlzqcgK6NR0wFCFRY= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= @@ -373,8 +373,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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"` }