github.com/crossplane/upjet@v1.3.0/pkg/terraform/provider_runner.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package terraform 6 7 import ( 8 "bufio" 9 "fmt" 10 "os" 11 "regexp" 12 "sync" 13 "time" 14 15 "github.com/crossplane/crossplane-runtime/pkg/logging" 16 "github.com/pkg/errors" 17 "k8s.io/utils/clock" 18 "k8s.io/utils/exec" 19 ) 20 21 const ( 22 // error messages 23 errFmtTimeout = "timed out after %v while waiting for the reattach configuration string" 24 25 // an example value would be: '{"registry.terraform.io/hashicorp/aws": {"Protocol": "grpc", "ProtocolVersion":5, "Pid":... "Addr":{"Network": "unix","String": "..."}}}' 26 fmtReattachEnv = `{"%s":{"Protocol":"grpc","ProtocolVersion":%d,"Pid":%d,"Test": true,"Addr":{"Network": "unix","String": "%s"}}}` 27 fmtSetEnv = "%s=%s" 28 envMagicCookie = "TF_PLUGIN_MAGIC_COOKIE" 29 // Terraform provider plugin expects this magic cookie in its environment 30 // (as the value of key TF_PLUGIN_MAGIC_COOKIE): 31 // https://github.com/hashicorp/terraform/blob/d35bc0531255b496beb5d932f185cbcdb2d61a99/internal/plugin/serve.go#L33 32 valMagicCookie = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2" 33 defaultProtocolVersion = 5 34 reattachTimeout = 1 * time.Minute 35 ) 36 37 var ( 38 regexReattachLine = regexp.MustCompile(`.*unix\|(.*)\|grpc.*`) 39 ) 40 41 // ProviderRunner is the interface for running 42 // Terraform native provider processes in the shared 43 // gRPC server mode 44 type ProviderRunner interface { 45 Start() (string, error) 46 Stop() error 47 } 48 49 // NoOpProviderRunner is a no-op ProviderRunner 50 type NoOpProviderRunner struct{} 51 52 // NewNoOpProviderRunner constructs a new NoOpProviderRunner 53 func NewNoOpProviderRunner() NoOpProviderRunner { 54 return NoOpProviderRunner{} 55 } 56 57 // Start takes no action 58 func (NoOpProviderRunner) Start() (string, error) { 59 return "", nil 60 } 61 62 // Stop takes no action 63 func (NoOpProviderRunner) Stop() error { 64 return nil 65 } 66 67 // SharedProvider runs the configured native provider plugin 68 // using the supplied command-line args 69 type SharedProvider struct { 70 nativeProviderPath string 71 nativeProviderArgs []string 72 reattachConfig string 73 nativeProviderName string 74 protocolVersion int 75 logger logging.Logger 76 executor exec.Interface 77 clock clock.Clock 78 mu *sync.Mutex 79 stopCh chan bool 80 } 81 82 // SharedProviderOption lets you configure the shared gRPC runner. 83 type SharedProviderOption func(runner *SharedProvider) 84 85 // WithNativeProviderArgs are the arguments to be passed to the native provider 86 func WithNativeProviderArgs(args ...string) SharedProviderOption { 87 return func(sp *SharedProvider) { 88 sp.nativeProviderArgs = args 89 } 90 } 91 92 // WithNativeProviderExecutor sets the process executor to be used 93 func WithNativeProviderExecutor(e exec.Interface) SharedProviderOption { 94 return func(sp *SharedProvider) { 95 sp.executor = e 96 } 97 } 98 99 // WithProtocolVersion sets the gRPC protocol version in use between 100 // the Terraform CLI and the native provider. 101 func WithProtocolVersion(protocolVersion int) SharedProviderOption { 102 return func(sp *SharedProvider) { 103 sp.protocolVersion = protocolVersion 104 } 105 } 106 107 // WithNativeProviderPath configures the Terraform provider executable path 108 // for the runner. 109 func WithNativeProviderPath(p string) SharedProviderOption { 110 return func(sp *SharedProvider) { 111 sp.nativeProviderPath = p 112 } 113 } 114 115 // WithNativeProviderName configures the Terraform provider name 116 // for the runner. 117 func WithNativeProviderName(n string) SharedProviderOption { 118 return func(sp *SharedProvider) { 119 sp.nativeProviderName = n 120 } 121 } 122 123 // WithNativeProviderLogger configures the logger for the runner. 124 func WithNativeProviderLogger(logger logging.Logger) SharedProviderOption { 125 return func(sp *SharedProvider) { 126 sp.logger = logger 127 } 128 } 129 130 // NewSharedProvider instantiates a SharedProvider runner with an 131 // OS executor using the supplied options. 132 func NewSharedProvider(opts ...SharedProviderOption) *SharedProvider { 133 sr := &SharedProvider{ 134 protocolVersion: defaultProtocolVersion, 135 executor: exec.New(), 136 clock: clock.RealClock{}, 137 mu: &sync.Mutex{}, 138 } 139 for _, o := range opts { 140 o(sr) 141 } 142 return sr 143 } 144 145 // Start starts a shared gRPC server if not already running 146 // A logger, native provider's path and command-line arguments to be 147 // passed to it must have been properly configured. 148 // Returns any errors encountered and the reattachment configuration for 149 // the native provider. 150 func (sr *SharedProvider) Start() (string, error) { //nolint:gocyclo 151 sr.mu.Lock() 152 defer sr.mu.Unlock() 153 log := sr.logger.WithValues("nativeProviderPath", sr.nativeProviderPath, "nativeProviderArgs", sr.nativeProviderArgs) 154 if sr.reattachConfig != "" { 155 log.Debug("Shared gRPC server is running...", "reattachConfig", sr.reattachConfig) 156 return sr.reattachConfig, nil 157 } 158 log.Debug("Provider runner not yet started. Will fork a new native provider.") 159 errCh := make(chan error, 1) 160 reattachCh := make(chan string, 1) 161 sr.stopCh = make(chan bool, 1) 162 163 go func() { 164 defer close(errCh) 165 defer close(reattachCh) 166 defer func() { 167 sr.mu.Lock() 168 sr.reattachConfig = "" 169 sr.mu.Unlock() 170 }() 171 //#nosec G204 no user input 172 cmd := sr.executor.Command(sr.nativeProviderPath, sr.nativeProviderArgs...) 173 cmd.SetEnv(append(os.Environ(), fmt.Sprintf(fmtSetEnv, envMagicCookie, valMagicCookie))) 174 stdout, err := cmd.StdoutPipe() 175 if err != nil { 176 errCh <- err 177 return 178 } 179 if err := cmd.Start(); err != nil { 180 errCh <- err 181 return 182 } 183 log.Debug("Forked new native provider.") 184 scanner := bufio.NewScanner(stdout) 185 for scanner.Scan() { 186 t := scanner.Text() 187 matches := regexReattachLine.FindStringSubmatch(t) 188 if matches == nil { 189 continue 190 } 191 reattachCh <- fmt.Sprintf(fmtReattachEnv, sr.nativeProviderName, sr.protocolVersion, os.Getpid(), matches[1]) 192 break 193 } 194 195 waitErrCh := make(chan error, 1) 196 go func() { 197 defer close(waitErrCh) 198 waitErrCh <- cmd.Wait() 199 }() 200 select { 201 case err := <-waitErrCh: 202 log.Info("Native Terraform provider process error", "error", err) 203 errCh <- err 204 case <-sr.stopCh: 205 cmd.Stop() 206 log.Debug("Stopped the provider runner.") 207 } 208 }() 209 210 select { 211 case reattachConfig := <-reattachCh: 212 sr.reattachConfig = reattachConfig 213 return sr.reattachConfig, nil 214 case err := <-errCh: 215 return "", err 216 case <-sr.clock.After(reattachTimeout): 217 return "", errors.Errorf(errFmtTimeout, reattachTimeout) 218 } 219 } 220 221 // Stop attempts to stop a shared gRPC server if it's already running. 222 func (sr *SharedProvider) Stop() error { 223 sr.mu.Lock() 224 defer sr.mu.Unlock() 225 sr.logger.Debug("Attempting to stop the provider runner.") 226 if sr.stopCh == nil { 227 return errors.New("shared provider process not started yet") 228 } 229 sr.stopCh <- true 230 close(sr.stopCh) 231 sr.stopCh = nil 232 return nil 233 }