Go plugin acquisition (#4060)
These changes implement `GetRequiredPlugins` for Go using a registry mechanism and an alternate entry point for `pulumi.Run`. Packages that require plugins are expected to register themselves with the Pulumi SDK. When `pulumi.Run` is used and the `PULUMI_PLUGINS` envvar is truthy, the program will dump a JSON-encoded description of its required plugins to stdout. The language host then uses this description to respond to
This commit is contained in:
parent
1354b17eb3
commit
8b46e71887
|
@ -1,5 +1,8 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
## HEAD (Unreleased)
|
||||
- Add support for plugin acquisition for Go programs
|
||||
[#4060](https://github.com/pulumi/pulumi/pull/4060)
|
||||
|
||||
## HEAD (Unreleased)
|
||||
- Update to Helm v3 in pulumi Docker image
|
||||
|
|
|
@ -730,7 +730,7 @@ func (pkg *pkgContext) genType(w io.Writer, obj *schema.ObjectType) {
|
|||
pkg.genOutputTypes(w, obj, pkg.details(obj))
|
||||
}
|
||||
|
||||
func (pkg *pkgContext) genInitFn(w io.Writer, types []*schema.ObjectType) {
|
||||
func (pkg *pkgContext) genTypeRegistrations(w io.Writer, types []*schema.ObjectType) {
|
||||
fmt.Fprintf(w, "func init() {\n")
|
||||
for _, obj := range types {
|
||||
name, details := pkg.tokenToType(obj.Token), pkg.details(obj)
|
||||
|
@ -912,6 +912,12 @@ func (pkg *pkgContext) genConfig(w io.Writer, variables []*schema.Property) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (pkg *pkgContext) genPackageRegistration(w io.Writer) {
|
||||
fmt.Fprintf(w, "func init() {\n")
|
||||
fmt.Fprintf(w, "\tpulumi.RegisterPackage(pulumi.PackageInfo{Name:\"%s\", Version:\"%s\"})\n", pkg.pkg.Name, pkg.pkg.Version.String())
|
||||
fmt.Fprintf(w, "}\n")
|
||||
}
|
||||
|
||||
// GoInfo holds information required to generate the Go SDK from a schema.
|
||||
type GoInfo struct {
|
||||
// Base path for package imports
|
||||
|
@ -1058,7 +1064,7 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
files[relPath] = []byte(contents)
|
||||
}
|
||||
|
||||
name := pkg.Name
|
||||
name, registerPackage := pkg.Name, pkg.Provider != nil
|
||||
for _, mod := range pkgMods {
|
||||
pkg := packages[mod]
|
||||
|
||||
|
@ -1130,11 +1136,21 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
pkg.genType(buffer, t)
|
||||
}
|
||||
|
||||
pkg.genInitFn(buffer, pkg.types)
|
||||
pkg.genTypeRegistrations(buffer, pkg.types)
|
||||
|
||||
setFile(path.Join(mod, "pulumiTypes.go"), buffer.String())
|
||||
}
|
||||
|
||||
// Package registration
|
||||
if registerPackage {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
pkg.genHeader(buffer, []string{"github.com/pulumi/pulumi/sdk/go/pulumi"}, nil)
|
||||
pkg.genPackageRegistration(buffer)
|
||||
|
||||
setFile(path.Join(mod, "pulumiManifest.go"), buffer.String())
|
||||
}
|
||||
|
||||
// Utilities
|
||||
if pkg.needsUtils {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
|
|
@ -37,6 +37,76 @@ import (
|
|||
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
|
||||
)
|
||||
|
||||
const unableToFindProgramTemplate = "unable to find program: %s"
|
||||
|
||||
// findExecutable attempts to find the needed executable in various locations on the
|
||||
// filesystem, eventually resorting to searching in $PATH.
|
||||
func findExecutable(program string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
program = fmt.Sprintf("%s.exe", program)
|
||||
}
|
||||
// look in the same directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
cwdProgram := filepath.Join(cwd, program)
|
||||
if fileInfo, err := os.Stat(cwdProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in CWD", program)
|
||||
return cwdProgram, nil
|
||||
}
|
||||
|
||||
// look in $GOPATH/bin
|
||||
if goPath := os.Getenv("GOPATH"); len(goPath) > 0 {
|
||||
goPathProgram := filepath.Join(goPath, "bin", program)
|
||||
if fileInfo, err := os.Stat(goPathProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in $GOPATH/bin", program)
|
||||
return goPathProgram, nil
|
||||
}
|
||||
}
|
||||
|
||||
// look in the $PATH somewhere
|
||||
if fullPath, err := exec.LookPath(program); err == nil {
|
||||
logging.V(5).Infof("program %s found in $PATH", program)
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
return "", errors.Errorf(unableToFindProgramTemplate, program)
|
||||
}
|
||||
|
||||
func findProgram(project string) (*exec.Cmd, error) {
|
||||
// The program to execute is simply the name of the project. This ensures good Go toolability, whereby
|
||||
// you can simply run `go install .` to build a Pulumi program prior to running it, among other benefits.
|
||||
// For ease of use, if we don't find a pre-built program, we attempt to invoke via 'go run' on behalf of the user.
|
||||
program, err := findExecutable(project)
|
||||
if err == nil {
|
||||
return exec.Command(program), nil
|
||||
}
|
||||
|
||||
const message = "problem executing program (could not run language executor)"
|
||||
if err.Error() == fmt.Sprintf(unableToFindProgramTemplate, project) {
|
||||
logging.V(5).Infof("Unable to find program %s in $PATH, attempting invocation via 'go run'", program)
|
||||
program, err = findExecutable("go")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
|
||||
// Fall back to 'go run' style execution
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
goFileSearchPattern := filepath.Join(cwd, "*.go")
|
||||
if matches, err := filepath.Glob(goFileSearchPattern); err != nil || len(matches) == 0 {
|
||||
return nil, errors.Errorf("Failed to find go files for 'go run' matching %s", goFileSearchPattern)
|
||||
}
|
||||
|
||||
return exec.Command(program, "run", cwd), nil
|
||||
}
|
||||
|
||||
// Launches the language host, which in turn fires up an RPC server implementing the LanguageRuntimeServer endpoint.
|
||||
func main() {
|
||||
var tracing string
|
||||
|
@ -90,46 +160,34 @@ func newLanguageHost(engineAddress, tracing string) pulumirpc.LanguageRuntimeSer
|
|||
// GetRequiredPlugins computes the complete set of anticipated plugins required by a program.
|
||||
func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context,
|
||||
req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) {
|
||||
return &pulumirpc.GetRequiredPluginsResponse{}, nil
|
||||
}
|
||||
|
||||
const unableToFindProgramTemplate = "unable to find program: %s"
|
||||
|
||||
// findProgram attempts to find the needed program in various locations on the
|
||||
// filesystem, eventually resorting to searching in $PATH.
|
||||
func findProgram(program string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
program = fmt.Sprintf("%s.exe", program)
|
||||
}
|
||||
|
||||
// look in the same directory
|
||||
cwd, err := os.Getwd()
|
||||
cmd, err := findProgram(req.GetProject())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to get current working directory")
|
||||
return nil, errors.Wrap(err, "failed to find program")
|
||||
}
|
||||
|
||||
cwdProgram := filepath.Join(cwd, program)
|
||||
if fileInfo, err := os.Stat(cwdProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in CWD", program)
|
||||
return cwdProgram, nil
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "PULUMI_PLUGINS=true")
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute program cmd")
|
||||
}
|
||||
|
||||
// look in $GOPATH/bin
|
||||
if goPath := os.Getenv("GOPATH"); len(goPath) > 0 {
|
||||
goPathProgram := filepath.Join(goPath, "bin", program)
|
||||
if fileInfo, err := os.Stat(goPathProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in $GOPATH/bin", program)
|
||||
return goPathProgram, nil
|
||||
}
|
||||
var infos map[string][]pulumi.PackageInfo
|
||||
if err := json.Unmarshal(stdout, &infos); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal result")
|
||||
}
|
||||
|
||||
// look in the $PATH somewhere
|
||||
if fullPath, err := exec.LookPath(program); err == nil {
|
||||
logging.V(5).Infof("program %s found in $PATH", program)
|
||||
return fullPath, nil
|
||||
var plugins []*pulumirpc.PluginDependency
|
||||
for _, info := range infos["plugins"] {
|
||||
plugins = append(plugins, &pulumirpc.PluginDependency{
|
||||
Name: info.Name,
|
||||
Kind: "resource",
|
||||
Version: info.Version,
|
||||
Server: info.Server,
|
||||
})
|
||||
}
|
||||
|
||||
return "", errors.Errorf(unableToFindProgramTemplate, program)
|
||||
return &pulumirpc.GetRequiredPluginsResponse{Plugins: plugins}, nil
|
||||
}
|
||||
|
||||
// RPC endpoint for LanguageRuntimeServer::Run
|
||||
|
@ -141,54 +199,14 @@ func (host *goLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest)
|
|||
return nil, errors.Wrap(err, "failed to prepare environment")
|
||||
}
|
||||
|
||||
// by default we try to run a named executable on the path, but we will fallback to 'go run' style execution
|
||||
goRunInvoke := false
|
||||
|
||||
// The program to execute is simply the name of the project. This ensures good Go toolability, whereby
|
||||
// you can simply run `go install .` to build a Pulumi program prior to running it, among other benefits.
|
||||
// For ease of use, if we don't find a pre-built program, we attempt to invoke via 'go run' on behalf of the user.
|
||||
program, err := findProgram(req.GetProject())
|
||||
cmd, err := findProgram(req.GetProject())
|
||||
if err != nil {
|
||||
const message = "problem executing program (could not run language executor)"
|
||||
if err.Error() == fmt.Sprintf(unableToFindProgramTemplate, req.GetProject()) {
|
||||
logging.V(5).Infof("Unable to find program %s in $PATH, attempting invocation via 'go run'", program)
|
||||
program, err = findProgram("go")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
goRunInvoke = true
|
||||
} else {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.V(5).Infof("language host launching process: %s", program)
|
||||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
var errResult string
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if goRunInvoke {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
goFileSearchPattern := filepath.Join(cwd, "*.go")
|
||||
if matches, err := filepath.Glob(goFileSearchPattern); err != nil || len(matches) == 0 {
|
||||
return nil, errors.Errorf("Failed to find go files for 'go run' matching %s", goFileSearchPattern)
|
||||
}
|
||||
|
||||
args := []string{"run", cwd}
|
||||
// go run $cwd
|
||||
cmd = exec.Command(program, args...)
|
||||
} else {
|
||||
cmd = exec.Command(program)
|
||||
}
|
||||
|
||||
cmd.Env = env
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
|
||||
var errResult string
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// If the program ran, but exited with a non-zero error code. This will happen often, since user
|
||||
|
|
|
@ -27,6 +27,8 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
var ErrPlugins = errors.New("pulumi: plugins requested")
|
||||
|
||||
// A RunOption is used to control the behavior of Run and RunErr.
|
||||
type RunOption func(*RunInfo)
|
||||
|
||||
|
@ -35,8 +37,13 @@ type RunOption func(*RunInfo)
|
|||
// If the program fails, the process will be terminated and the function will not return.
|
||||
func Run(body RunFunc, opts ...RunOption) {
|
||||
if err := RunErr(body, opts...); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err != ErrPlugins {
|
||||
fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printRequiredPlugins()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +53,9 @@ func RunErr(body RunFunc, opts ...RunOption) error {
|
|||
// Parse the info out of environment variables. This is a lame contract with the caller, but helps to keep
|
||||
// boilerplate to a minimum in the average Pulumi Go program.
|
||||
info := getEnvInfo()
|
||||
if info.getPlugins {
|
||||
return ErrPlugins
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&info)
|
||||
|
@ -121,6 +131,7 @@ type RunInfo struct {
|
|||
MonitorAddr string
|
||||
EngineAddr string
|
||||
Mocks MockResourceMonitor
|
||||
getPlugins bool
|
||||
}
|
||||
|
||||
// getEnvInfo reads various program information from the process environment.
|
||||
|
@ -128,6 +139,7 @@ func getEnvInfo() RunInfo {
|
|||
// Most of the variables are just strings, and we can read them directly. A few of them require more parsing.
|
||||
parallel, _ := strconv.Atoi(os.Getenv(EnvParallel))
|
||||
dryRun, _ := strconv.ParseBool(os.Getenv(EnvDryRun))
|
||||
getPlugins, _ := strconv.ParseBool(os.Getenv(envPlugins))
|
||||
|
||||
var config map[string]string
|
||||
if cfg := os.Getenv(EnvConfig); cfg != "" {
|
||||
|
@ -142,6 +154,7 @@ func getEnvInfo() RunInfo {
|
|||
DryRun: dryRun,
|
||||
MonitorAddr: os.Getenv(EnvMonitor),
|
||||
EngineAddr: os.Getenv(EnvEngine),
|
||||
getPlugins: getPlugins,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,4 +173,28 @@ const (
|
|||
EnvMonitor = "PULUMI_MONITOR"
|
||||
// EnvEngine is the envvar used to read the current Pulumi engine RPC address.
|
||||
EnvEngine = "PULUMI_ENGINE"
|
||||
// envPlugins is the envvar used to request that the Pulumi program print its set of required plugins and exit.
|
||||
envPlugins = "PULUMI_PLUGINS"
|
||||
)
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
}
|
||||
|
||||
var packageRegistry = map[PackageInfo]struct{}{}
|
||||
|
||||
func RegisterPackage(info PackageInfo) {
|
||||
packageRegistry[info] = struct{}{}
|
||||
}
|
||||
|
||||
func printRequiredPlugins() {
|
||||
plugins := []PackageInfo{}
|
||||
for info := range packageRegistry {
|
||||
plugins = append(plugins, info)
|
||||
}
|
||||
|
||||
err := json.NewEncoder(os.Stdout).Encode(map[string]interface{}{"plugins": plugins})
|
||||
contract.IgnoreError(err)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue