diff --git a/builder/amazon/common/ssm/driver.go b/builder/amazon/common/ssm/driver.go new file mode 100644 index 000000000..79e662eb3 --- /dev/null +++ b/builder/amazon/common/ssm/driver.go @@ -0,0 +1,111 @@ +package ssm + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/aws/aws-sdk-go/service/ssm/ssmiface" + "github.com/hashicorp/packer/common/retry" + "github.com/hashicorp/packer/helper/builder/localexec" + "github.com/hashicorp/packer/packer" +) + +type Session struct { + SvcClient ssmiface.SSMAPI + Region string + Input ssm.StartSessionInput +} + +// Returns true if the error matches all these conditions: +// * err is of type awserr.Error +// * Error.Code() matches code +// * Error.Message() contains message +func isAWSErr(err error, code string, message string) bool { + if err, ok := err.(awserr.Error); ok { + return err.Code() == code && strings.Contains(err.Message(), message) + } + return false +} + +// getCommand return a valid ordered set of arguments to pass to the driver command. +func (s Session) getCommand(ctx context.Context) ([]string, string, error) { + var session *ssm.StartSessionOutput + err := retry.Config{ + ShouldRetry: func(err error) bool { return isAWSErr(err, "TargetNotConnected", "") }, + RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear, + }.Run(ctx, func(ctx context.Context) (err error) { + session, err = s.SvcClient.StartSessionWithContext(ctx, &s.Input) + return err + }) + + if err != nil { + return nil, "", err + } + + if session == nil { + return nil, "", fmt.Errorf("an active Amazon SSM Session is required before trying to open a session tunnel") + } + + // AWS session-manager-plugin requires a valid session be passed in JSON. + sessionDetails, err := json.Marshal(session) + if err != nil { + return nil, *session.SessionId, fmt.Errorf("error encountered in reading session details %s", err) + } + + // AWS session-manager-plugin requires the parameters used in the session to be passed in JSON as well. + sessionParameters, err := json.Marshal(s.Input) + if err != nil { + return nil, "", fmt.Errorf("error encountered in reading session parameter details %s", err) + } + + // Args must be in this order + args := []string{ + string(sessionDetails), + s.Region, + "StartSession", + "", // ProfileName + string(sessionParameters), + *session.StreamUrl, + } + return args, *session.SessionId, nil +} + +// Start an interactive Systems Manager session with a remote instance via the +// AWS session-manager-plugin. To terminate the session you must cancell the +// context. If you do not wish to terminate the session manually: calling +// StopSession on a instance of this driver will terminate the active session +// created from calling StartSession. +func (s Session) Start(ctx context.Context, ui packer.Ui) error { + for ctx.Err() == nil { + log.Printf("ssm: Starting PortForwarding session to instance %q", *s.Input.Target) + args, sessionID, err := s.getCommand(ctx) + if sessionID != "" { + defer func() { + _, err := s.SvcClient.TerminateSession(&ssm.TerminateSessionInput{SessionId: aws.String(sessionID)}) + if err != nil { + err = fmt.Errorf("Error terminating SSM Session %q. Please terminate the session manually: %s", sessionID, err) + } + }() + } + if err != nil { + return err + } + + cmd := exec.CommandContext(ctx, "session-manager-plugin", args...) + + ui.Message(fmt.Sprintf("Starting portForwarding session %q.", sessionID)) + err = localexec.RunAndStream(cmd, ui, nil) + if err != nil { + ui.Error(err.Error()) + } + } + return nil +} diff --git a/builder/amazon/common/ssm_driver.go b/builder/amazon/common/ssm_driver.go index 9b95b6fe3..2a8dfbd53 100644 --- a/builder/amazon/common/ssm_driver.go +++ b/builder/amazon/common/ssm_driver.go @@ -2,17 +2,9 @@ package common import ( "context" - "encoding/json" - "fmt" - "log" - "os/exec" - "time" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm/ssmiface" - "github.com/hashicorp/packer/common/retry" - "github.com/mitchellh/iochan" ) const ( @@ -34,212 +26,9 @@ type SSMDriver struct { session *ssm.StartSessionOutput sessionParams ssm.StartSessionInput pluginCmdFunc func(context.Context) error - - retryAfterTermination chan bool } func NewSSMDriver(config SSMDriverConfig) *SSMDriver { d := SSMDriver{SSMDriverConfig: config} return &d } - -// StartSession starts an interactive Systems Manager session with a remote instance via the AWS session-manager-plugin -// This ssm.StartSessionOutput returned by this function can be used for terminating the session manually. If you do -// not wish to manage the session manually calling StopSession on a instance of this driver will terminate the active session -// created from calling StartSession. -func (d *SSMDriver) StartSession(ctx context.Context, input ssm.StartSessionInput) (*ssm.StartSessionOutput, error) { - log.Printf("Starting PortForwarding session to instance %q", aws.StringValue(input.Target)) - - output, err := d.StartSessionWithContext(ctx, input) - if err != nil { - return nil, fmt.Errorf("error encountered in starting session for instance %q: %s", aws.StringValue(input.Target), err) - } - - d.retryAfterTermination = make(chan bool, 1) - // Starts go routine that will keep listening to channels and retry the session creation/connection when needed. - // The log polling process will add data to the channels whenever a retryable error happens to the session or if it's terminated. - go func(ctx context.Context, driver *SSMDriver, input ssm.StartSessionInput) { - for { - select { - case <-ctx.Done(): - return - case r, ok := <-driver.retryAfterTermination: - if !ok { - return - } - if r { - log.Printf("[DEBUG] Attempting to restablishing SSM connection") - _, _ = driver.StartSession(ctx, input) - // End this routine. Another routine will start. - return - } - } - } - }(ctx, d, input) - - d.session = output - d.sessionParams = input - - if d.pluginCmdFunc == nil { - d.pluginCmdFunc = d.openTunnelForSession - } - - if err := d.pluginCmdFunc(ctx); err != nil { - return nil, fmt.Errorf("error encountered in starting session for instance %q: %s", aws.StringValue(input.Target), err) - } - - return d.session, nil -} - -func (d *SSMDriver) StartSessionWithContext(ctx context.Context, input ssm.StartSessionInput) (*ssm.StartSessionOutput, error) { - var output *ssm.StartSessionOutput - err := retry.Config{ - ShouldRetry: func(err error) bool { return IsAWSErr(err, "TargetNotConnected", "") }, - RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear, - }.Run(ctx, func(ctx context.Context) (err error) { - output, err = d.SvcClient.StartSessionWithContext(ctx, &input) - return err - }) - - return output, err -} - -func (d *SSMDriver) openTunnelForSession(ctx context.Context) error { - args, err := d.Args() - if err != nil { - return fmt.Errorf("error encountered validating session details: %s", err) - } - - cmd := exec.CommandContext(ctx, sessionManagerPluginName, args...) - - // Let's build up our logging - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - - // Create the channels we'll use for data - stdoutCh := iochan.DelimReader(stdout, '\n') - stderrCh := iochan.DelimReader(stderr, '\n') - - /* Loop and get all our output - This particular logger will continue to run through an entire Packer run. - The decision to continue logging is due to the fact that session-manager-plugin - doesn't give a good way of knowing if the command failed or was successful other - than looking at the logs. Seeing as the plugin is updated frequently and that the - log information is a bit sparse this logger will indefinitely relying on other - steps to fail if the tunnel is unable to be created. If successful then the user - will get more information on the tunnel connection when running in a debug mode. - */ - go func(ctx context.Context, prefix string) { - for { - select { - case <-ctx.Done(): - return - case errorOut, ok := <-stderrCh: - if !ok { - stderrCh = nil - break - } - - if errorOut == "" { - continue - } - - log.Printf("[ERROR] %s: %s", prefix, errorOut) - case output, ok := <-stdoutCh: - if !ok { - stdoutCh = nil - break - } - - if output == "" { - continue - } - log.Printf("[DEBUG] %s: %s", prefix, output) - } - - if stdoutCh == nil && stderrCh == nil { - // It's possible that a remote SSM session was terminated prematurely due to a system reboot, or a termination - // on the AWS console, or that the SSMAgent service was stopped on the instance. This will try to reconnect - // until the build eventually fails or hits the ssh_timeout threshold. - log.Printf("[DEBUG] %s: %s", prefix, "active session has been terminated; stopping all log polling processes.") - - // A call to StopSession will nullify any active sessions so only retry if StopSession has not be called, - // since StopSession will also close the retryAfterTermination channel - if d.session != nil { - d.retryAfterTermination <- true - } - return - } - } - }(ctx, sessionManagerPluginName) - - log.Printf("[DEBUG %s] opening session tunnel to instance %q for session %q", sessionManagerPluginName, - aws.StringValue(d.sessionParams.Target), - aws.StringValue(d.session.SessionId), - ) - - if err := cmd.Start(); err != nil { - err = fmt.Errorf("error encountered when calling %s: %s\n", sessionManagerPluginName, err) - return err - } - - return nil -} - -// StopSession terminates an active Session Manager session -func (d *SSMDriver) StopSession() error { - if d.session == nil || d.session.SessionId == nil { - return fmt.Errorf("Unable to find a valid session to instance %q; skipping the termination step", - aws.StringValue(d.sessionParams.Target)) - } - - // Store session id before we nullify any active sessions - sid := aws.StringValue(d.session.SessionId) - - // Stop retry polling process to avoid unwanted retries at this point - d.session = nil - close(d.retryAfterTermination) - _, err := d.SvcClient.TerminateSession(&ssm.TerminateSessionInput{SessionId: aws.String(sid)}) - if err != nil { - err = fmt.Errorf("Error terminating SSM Session %q. Please terminate the session manually: %s", sid, err) - } - - return err -} - -// Args validates the driver inputs before returning an ordered set of arguments to pass to the driver command. -func (d *SSMDriver) Args() ([]string, error) { - if d.session == nil { - return nil, fmt.Errorf("an active Amazon SSM Session is required before trying to open a session tunnel") - } - - // AWS session-manager-plugin requires a valid session be passed in JSON. - sessionDetails, err := json.Marshal(d.session) - if err != nil { - return nil, fmt.Errorf("error encountered in reading session details %s", err) - } - - // AWS session-manager-plugin requires the parameters used in the session to be passed in JSON as well. - sessionParameters, err := json.Marshal(d.sessionParams) - if err != nil { - return nil, fmt.Errorf("error encountered in reading session parameter details %s", err) - } - - // Args must be in this order - args := []string{ - string(sessionDetails), - d.Region, - sessionCommand, - d.ProfileName, - string(sessionParameters), - d.SvcEndpoint, - } - - return args, nil -} diff --git a/builder/amazon/common/ssm_mock_funcs.go b/builder/amazon/common/ssm_mock_funcs.go index 1d358baaa..8ffb06a25 100644 --- a/builder/amazon/common/ssm_mock_funcs.go +++ b/builder/amazon/common/ssm_mock_funcs.go @@ -21,6 +21,7 @@ func (svc *MockSSMSvc) StartSessionWithContext(ctx aws.Context, input *ssm.Start svc.StartSessionCalled = true return MockStartSessionOutput(), svc.StartSessionError } + func (svc *MockSSMSvc) TerminateSession(input *ssm.TerminateSessionInput) (*ssm.TerminateSessionOutput, error) { svc.TerminateSessionCalled = true return new(ssm.TerminateSessionOutput), svc.TerminateSessionError diff --git a/builder/amazon/common/step_create_ssm_tunnel.go b/builder/amazon/common/step_create_ssm_tunnel.go index c29a4dc5b..4149acf4c 100644 --- a/builder/amazon/common/step_create_ssm_tunnel.go +++ b/builder/amazon/common/step_create_ssm_tunnel.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ssm" + pssm "github.com/hashicorp/packer/builder/amazon/common/ssm" "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" @@ -24,6 +25,7 @@ type StepCreateSSMTunnel struct { instanceId string PauseBeforeSSM time.Duration driver *SSMDriver + stopSSMCommand func() } // Run executes the Packer build step that creates a session tunnel. @@ -73,30 +75,38 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag) driver := SSMDriver{SSMDriverConfig: cfg} s.driver = &driver } + state.Put("sessionPort", s.LocalPortNumber) input := s.BuildTunnelInputForInstance(s.instanceId) - _, err := s.driver.StartSession(ctx, input) - if err != nil { - err = fmt.Errorf("error encountered in establishing a tunnel %s", err) - ui.Error(err.Error()) - state.Put("error", err) - return multistep.ActionHalt - } - ui.Message(fmt.Sprintf("PortForwarding session %q has been started", s.instanceId)) - state.Put("sessionPort", s.LocalPortNumber) + ssmCtx, ssmCancel := context.WithCancel(ctx) + s.stopSSMCommand = ssmCancel + + go func() { + err := pssm.Session{ + SvcClient: s.driver.SvcClient, + Input: input, + Region: s.driver.Region, + }.Start(ssmCtx, ui) + + if err != nil { + err = fmt.Errorf("error encountered in establishing a tunnel %s", err) + ui.Error(err.Error()) + state.Put("error", err) + } + }() + return multistep.ActionContinue } // Cleanup terminates an active session on AWS, which in turn terminates the associated tunnel process running on the local machine. func (s *StepCreateSSMTunnel) Cleanup(state multistep.StateBag) { - ui := state.Get("ui").(packer.Ui) if !s.SSMAgentEnabled { return } - if err := s.driver.StopSession(); err != nil { - ui.Error(err.Error()) + if s.stopSSMCommand != nil { + s.stopSSMCommand() } } diff --git a/builder/amazon/error/utils.go b/builder/amazon/error/utils.go new file mode 100644 index 000000000..dcf98a825 --- /dev/null +++ b/builder/amazon/error/utils.go @@ -0,0 +1,18 @@ +package error + +import ( + "strings" + + "github.com/aws/aws-sdk-go/aws/awserr" +) + +// Returns true if the err matches all these conditions: +// * err is of type awserr.Error +// * Error.Code() matches code +// * Error.Message() contains message +func Matches(err error, code string, message string) bool { + if err, ok := err.(awserr.Error); ok { + return err.Code() == code && strings.Contains(err.Message(), message) + } + return false +} diff --git a/go.mod b/go.mod index 2ef26cf09..c53e9c9d8 100644 --- a/go.mod +++ b/go.mod @@ -93,7 +93,6 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-testing-interface v1.0.3 // indirect github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed - github.com/mitchellh/gox v1.0.1 // indirect github.com/mitchellh/iochan v1.0.0 github.com/mitchellh/mapstructure v1.2.3 github.com/mitchellh/panicwrap v1.0.0 @@ -104,7 +103,6 @@ require ( github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/olekukonko/tablewriter v0.0.0-20180105111133-96aac992fc8b github.com/oracle/oci-go-sdk v18.0.0+incompatible - github.com/outscale/osc-go v0.0.1 // indirect github.com/outscale/osc-sdk-go/osc v0.0.0-20200722135656-d654809d0699 github.com/packer-community/winrmcp v0.0.0-20180921204643-0fd363d6159a github.com/pierrec/lz4 v2.0.5+incompatible diff --git a/go.sum b/go.sum index b4afda482..8a908afec 100644 --- a/go.sum +++ b/go.sum @@ -126,7 +126,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.16.22/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.26.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.8 h1:4BHbh8K3qKmcnAgToZ2LShldRF9inoqIBccpCLNCy3I= github.com/aws/aws-sdk-go v1.30.8/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= @@ -377,7 +376,6 @@ github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= @@ -512,8 +510,6 @@ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZX github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= -github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -538,9 +534,6 @@ github.com/olekukonko/tablewriter v0.0.0-20180105111133-96aac992fc8b/go.mod h1:v github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/oracle/oci-go-sdk v18.0.0+incompatible h1:FLV4KixsVfF3rwyVTMI6Ryp/Q+OSb9sR5TawbfjFLN4= github.com/oracle/oci-go-sdk v18.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/outscale/osc-go v0.0.1 h1:hvBtORyu7sWSKW1norGlfIP8C7c2aegI2Vkq75SRPCE= -github.com/outscale/osc-go v0.0.1/go.mod h1:hJLmXzqU/t07qQYh90I0TqZzu9s85Zs6FMrxk3ukiFM= -github.com/outscale/osc-sdk-go v1.2.0 h1:1HKr6OMLLVW4w6KQuiQwYZjhNaVz9mNzy/W3KW+zgnA= github.com/outscale/osc-sdk-go/osc v0.0.0-20200722135656-d654809d0699 h1:SHe9i7h5cHe+cB77fQ6lsEgIwKg3ckNU90P03CjGMnI= github.com/outscale/osc-sdk-go/osc v0.0.0-20200722135656-d654809d0699/go.mod h1:5AqqNH1X8zCHescKVlpSHRzrat1KCKDXqZoQPe8fY3A= github.com/packer-community/winrmcp v0.0.0-20180921204643-0fd363d6159a h1:A3QMuteviunoaY/8ex+RKFqwhcZJ/Cf3fCW3IwL2wx4= @@ -636,12 +629,8 @@ github.com/vmware/govmomi v0.23.1/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAu github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= github.com/xanzy/go-cloudstack v0.0.0-20190526095453-42f262b63ed0 h1:NJrcIkdzq0C3I8ypAZwFE9RHtGbfp+mJvqIcoFATZuk= github.com/xanzy/go-cloudstack v0.0.0-20190526095453-42f262b63ed0/go.mod h1:sBh287mCRwCz6zyXHMmw7sSZGPohVpnx+o+OY4M+i3A= -github.com/yandex-cloud/go-genproto v0.0.0-20200608085315-d6e7ef5ceb97 h1:DoqSUxQkBLislVgA1qkM0u7g04It4VRMidyLBH/O/as= -github.com/yandex-cloud/go-genproto v0.0.0-20200608085315-d6e7ef5ceb97/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-genproto v0.0.0-20200915125933-33de72a328bd h1:o4pvS7D4OErKOM6y+/q6IfOa65OaentKbEDh1ABirE8= github.com/yandex-cloud/go-genproto v0.0.0-20200915125933-33de72a328bd/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= -github.com/yandex-cloud/go-sdk v0.0.0-20200610100221-ae86895efb97 h1:8KwSw9xtQBeyeX1EpOlOjRc0JaHlh8B8GglKA6iXt08= -github.com/yandex-cloud/go-sdk v0.0.0-20200610100221-ae86895efb97/go.mod h1:3p2xVpQrHyPxV4UCKnKozt9n+g1LRENOQ33CH8rqLnY= github.com/yandex-cloud/go-sdk v0.0.0-20200921111412-ef15ded2014c h1:LJrgyICodRAgtBvOO2eCbhDDIoaJgeLa1tGQecqW9ac= github.com/yandex-cloud/go-sdk v0.0.0-20200921111412-ef15ded2014c/go.mod h1:Zn/U9YKH0w8n83ezLps5eB6Jftc4gSoZWxVR8hgXgoY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=