pulumi/pkg/cmd/pulumi/package_gen_sdk.go

227 lines
7.0 KiB
Go

// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/blang/semver"
"github.com/spf13/cobra"
javagen "github.com/pulumi/pulumi-java/pkg/codegen/java"
"github.com/pulumi/pulumi/pkg/v3/codegen/dotnet"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
func newGenSdkCommand() *cobra.Command {
var overlays string
var language string
var out string
var version string
var local bool
cmd := &cobra.Command{
Use: "gen-sdk <schema_source> [provider parameters]",
Args: cobra.MinimumNArgs(1),
Short: "Generate SDK(s) from a package or schema",
Long: `Generate SDK(s) from a package or schema.
<schema_source> can be a package name or the path to a plugin binary or folder.
If a folder either the plugin binary must match the folder name (e.g. 'aws' and 'pulumi-resource-aws')` +
` or it must have a PulumiPlugin.yaml file specifying the runtime to use.`,
Run: runCmdFunc(func(cmd *cobra.Command, args []string) error {
source := args[0]
d := diag.DefaultSink(os.Stdout, os.Stderr, diag.FormatOptions{Color: cmdutil.GetGlobalColorization()})
pkg, err := schemaFromSchemaSource(cmd.Context(), source, args[1:])
if err != nil {
return err
}
if version != "" {
pkgVersion, err := semver.Parse(version)
if err != nil {
return fmt.Errorf("invalid version %q: %w", version, err)
}
if pkg.Version != nil {
d.Infof(diag.Message("", "overriding package version %s with %s"), pkg.Version, pkgVersion)
}
pkg.Version = &pkgVersion
}
// Normalize from well known language names the the matching runtime names.
switch language {
case "csharp", "c#":
language = "dotnet"
case "typescript":
language = "nodejs"
}
if language == "all" {
for _, lang := range []string{"dotnet", "go", "java", "nodejs", "python"} {
err := genSDK(lang, out, pkg, overlays, local)
if err != nil {
return err
}
}
return nil
}
return genSDK(language, out, pkg, overlays, local)
}),
}
cmd.Flags().StringVarP(&language, "language", "", "all",
"The SDK language to generate: [nodejs|python|go|dotnet|java|all]")
cmd.Flags().StringVarP(&out, "out", "o", "./sdk",
"The directory to write the SDK to")
cmd.Flags().StringVar(&overlays, "overlays", "", "A folder of extra overlay files to copy to the generated SDK")
cmd.Flags().StringVar(&version, "version", "", "The provider plugin version to generate the SDK for")
cmd.Flags().BoolVar(&local, "local", false, "Generate an SDK appropriate for local usage")
contract.AssertNoErrorf(cmd.Flags().MarkHidden("overlays"), `Could not mark "overlay" as hidden`)
return cmd
}
func genSDK(language, out string, pkg *schema.Package, overlays string, local bool) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("get current working directory: %w", err)
}
writeWrapper := func(
generatePackage func(string, *schema.Package, map[string][]byte) (map[string][]byte, error),
) func(string, *schema.Package, map[string][]byte) error {
return func(directory string, p *schema.Package, extraFiles map[string][]byte) error {
m, err := generatePackage("pulumi", p, extraFiles)
if err != nil {
return err
}
err = os.RemoveAll(directory)
if err != nil && !os.IsNotExist(err) {
return err
}
for k, v := range m {
path := filepath.Join(directory, k)
err := os.MkdirAll(filepath.Dir(path), 0o700)
if err != nil {
return err
}
err = os.WriteFile(path, v, 0o600)
if err != nil {
return err
}
}
return nil
}
}
var generatePackage func(string, *schema.Package, map[string][]byte) error
switch language {
case "dotnet":
generatePackage = writeWrapper(func(t string, p *schema.Package, e map[string][]byte) (map[string][]byte, error) {
return dotnet.GeneratePackage(t, p, e, nil)
})
case "java":
generatePackage = writeWrapper(func(t string, p *schema.Package, e map[string][]byte) (map[string][]byte, error) {
return javagen.GeneratePackage(t, p, e, local)
})
default:
generatePackage = func(directory string, pkg *schema.Package, extraFiles map[string][]byte) error {
// Ensure the target directory is clean, but created.
err = os.RemoveAll(directory)
if err != nil && !os.IsNotExist(err) {
return err
}
err := os.MkdirAll(directory, 0o700)
if err != nil {
return err
}
jsonBytes, err := pkg.MarshalJSON()
if err != nil {
return err
}
pCtx, err := newPluginContext(cwd)
if err != nil {
return fmt.Errorf("create plugin context: %w", err)
}
defer contract.IgnoreClose(pCtx.Host)
programInfo := plugin.NewProgramInfo(cwd, cwd, ".", nil)
languagePlugin, err := pCtx.Host.LanguageRuntime(language, programInfo)
if err != nil {
return err
}
loader := schema.NewPluginLoader(pCtx.Host)
loaderServer := schema.NewLoaderServer(loader)
grpcServer, err := plugin.NewServer(pCtx, schema.LoaderRegistration(loaderServer))
if err != nil {
return err
}
defer contract.IgnoreClose(grpcServer)
diags, err := languagePlugin.GeneratePackage(directory, string(jsonBytes), extraFiles, grpcServer.Addr(), nil, local)
if err != nil {
return err
}
// These diagnostics come directly from the converter and so _should_ be user friendly. So we're just
// going to print them.
printDiagnostics(pCtx.Diag, diags)
if diags.HasErrors() {
// If we've got error diagnostics then package generation failed, we've printed the error above so
// just return a plain message here.
return errors.New("generation failed")
}
return nil
}
}
extraFiles := make(map[string][]byte)
if overlays != "" {
fsys := os.DirFS(filepath.Join(overlays, language))
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
contents, err := fs.ReadFile(fsys, path)
if err != nil {
return fmt.Errorf("read overlay file %q: %w", path, err)
}
extraFiles[path] = contents
return nil
})
if err != nil {
return fmt.Errorf("read overlay directory %q: %w", overlays, err)
}
}
root := filepath.Join(out, language)
err = generatePackage(root, pkg, extraFiles)
if err != nil {
return err
}
return nil
}