pulumi/pkg/cmd/pulumi/package_add.go

384 lines
12 KiB
Go

// Copyright 2024, 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"
"os"
"path/filepath"
"runtime"
"strings"
go_gen "github.com/pulumi/pulumi/pkg/v3/codegen/go"
"github.com/pulumi/pulumi/pkg/v3/codegen/python"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
pkgWorkspace "github.com/pulumi/pulumi/pkg/v3/workspace"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Constructs the `pulumi package add` command.
func newPackageAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add <provider> [provider-parameter...]",
Args: cobra.MinimumNArgs(1),
Short: "Add a package to your Pulumi project",
Long: `Add a package to your Pulumi project.
For parameterized providers, parameters should be specified as additional arguments.
The exact format of parameters is provider-specific; consult the provider's
documentation for more information. If the parameters include flags that begin with
dashes, you may need to use '--' to separate the provider name from the parameters,
as in:
pulumi package add <provider> -- --provider-parameter-flag value`,
Run: runCmdFunc(func(cmd *cobra.Command, args []string) error {
ws := pkgWorkspace.Instance
proj, root, err := ws.ReadProject()
if err != nil && errors.Is(err, workspace.ErrProjectNotFound) {
return err
}
language := proj.Runtime.Name()
ctx := cmd.Context()
plugin := args[0]
parameters := args[1:]
pkg, err := schemaFromSchemaSource(ctx, plugin, parameters)
if err != nil {
return fmt.Errorf("failed to get schema: %w", err)
}
tempOut, err := os.MkdirTemp("", "pulumi-package-add-")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
local := true
err = genSDK(
language,
tempOut,
pkg,
"", /*overlays*/
local, /*local*/
)
if err != nil {
return fmt.Errorf("failed to generate SDK: %w", err)
}
out := filepath.Join(root, "sdks")
err = os.MkdirAll(out, 0o755)
if err != nil {
return fmt.Errorf("failed to create directory for SDK: %w", err)
}
out = filepath.Join(out, pkg.Name)
err = copyAll(out, filepath.Join(tempOut, language))
if err != nil {
return fmt.Errorf("failed to move SDK to project: %w", err)
}
err = os.RemoveAll(tempOut)
if err != nil {
return fmt.Errorf("failed to remove temporary directory: %w", err)
}
return printLinkInstructions(ws, language, root, pkg, out)
}),
}
return cmd
}
// Prints instructions for linking a locally generated SDK to an existing
// project, in the absence of us attempting to perform this linking automatically.
func printLinkInstructions(
ws pkgWorkspace.Context, language string, root string, pkg *schema.Package, out string,
) error {
switch language {
case "nodejs":
return printNodejsLinkInstructions(ws, root, pkg, out)
case "python":
return printPythonLinkInstructions(ws, root, pkg, out)
case "go":
return printGoLinkInstructions(root, pkg, out)
case "dotnet":
return printDotnetLinkInstructions(root, pkg, out)
case "java":
return printJavaLinkInstructions(root, pkg, out)
default:
break
}
return nil
}
// Prints instructions for linking a locally generated SDK to an existing NodeJS
// project, in the absence of us attempting to perform this linking automatically.
func printNodejsLinkInstructions(ws pkgWorkspace.Context, root string, pkg *schema.Package, out string) error {
fmt.Printf("Successfully generated a Nodejs SDK for the %s package at %s\n", pkg.Name, out)
fmt.Println()
fmt.Println("To use this SDK in your Nodejs project, run the following command:")
fmt.Println()
proj, _, err := ws.ReadProject()
if err != nil {
return err
}
relOut, err := filepath.Rel(root, out)
if err != nil {
return err
}
packageSpecifier := fmt.Sprintf("%s@file:%s", pkg.Name, relOut)
var addCmd string
options := proj.Runtime.Options()
if packagemanager, ok := options["packagemanager"]; ok {
if pm, ok := packagemanager.(string); ok {
switch pm {
case "npm":
fallthrough
case "yarn":
fallthrough
case "pnpm":
addCmd = pm + " add " + packageSpecifier
default:
return fmt.Errorf("unsupported package manager: %s", pm)
}
} else {
fmt.Println("packagemanager", packagemanager)
return fmt.Errorf("packagemanager option must be a string: %v", packagemanager)
}
} else {
// Assume npm if no packagemanager is specified
addCmd = "npm add " + packageSpecifier
}
fmt.Println(" " + addCmd)
fmt.Println()
useTypescript := true
if typescript, ok := options["typescript"]; ok {
if val, ok := typescript.(bool); ok {
useTypescript = val
}
}
if useTypescript {
fmt.Println("You can then import the SDK in your TypeScript code with:")
fmt.Println()
fmt.Printf(" import * as %s from \"%s\";\n", pkg.Name, pkg.Name)
} else {
fmt.Println("You can then import the SDK in your Javascript code with:")
fmt.Println()
fmt.Printf(" const %s = require(\"%s\");\n", pkg.Name, pkg.Name)
}
fmt.Println()
return nil
}
// Prints instructions for linking a locally generated SDK to an existing Python
// project, in the absence of us attempting to perform this linking automatically.
func printPythonLinkInstructions(ws pkgWorkspace.Context, root string, pkg *schema.Package, out string) error {
fmt.Printf("Successfully generated a Python SDK for the %s package at %s\n", pkg.Name, out)
fmt.Println()
fmt.Println("To use this SDK in your Python project, run the following command:")
fmt.Println()
proj, _, err := ws.ReadProject()
if err != nil {
return err
}
packageSpecifier, err := filepath.Rel(root, out)
if err != nil {
return err
}
pipInstructions := func() {
fmt.Printf(" echo %s >> requirements.txt\n\n", packageSpecifier)
fmt.Printf(" pulumi install\n")
}
options := proj.Runtime.Options()
if toolchain, ok := options["toolchain"]; ok {
if tc, ok := toolchain.(string); ok {
switch tc {
case "pip":
pipInstructions()
case "poetry":
fmt.Println(" poetry add " + packageSpecifier)
default:
return fmt.Errorf("unsupported package manager: %s", tc)
}
} else {
return fmt.Errorf("packagemanager option must be a string: %v", toolchain)
}
} else {
// Assume pip if no packagemanager is specified
pipInstructions()
}
pyInfo, ok := pkg.Language["python"].(python.PackageInfo)
var importName string
if ok && pyInfo.PackageName != "" {
importName = pyInfo.PackageName
} else {
importName = strings.ReplaceAll(pkg.Name, "-", "_")
}
fmt.Println()
fmt.Println("You can then import the SDK in your Python code with:")
fmt.Println()
fmt.Printf(" import pulumi_%s as %s\n", importName, importName)
fmt.Println()
return nil
}
// Prints instructions for linking a locally generated SDK to an existing Go
// project, in the absence of us attempting to perform this linking automatically.
func printGoLinkInstructions(root string, pkg *schema.Package, out string) error {
fmt.Printf("Successfully generated a Go SDK for the %s package at %s\n", pkg.Name, out)
fmt.Println()
fmt.Println("To use this SDK in your Go project, run the following command:")
fmt.Println()
relOut, err := filepath.Rel(root, out)
if err != nil {
return err
}
if runtime.GOOS == "windows" {
relOut = ".\\" + relOut
} else {
relOut = "./" + relOut
}
if err := pkg.ImportLanguages(map[string]schema.Language{"go": go_gen.Importer}); err != nil {
return err
}
goInfo, ok := pkg.Language["go"].(go_gen.GoPackageInfo)
if !ok {
return errors.New("failed to import go language info")
}
fmt.Printf(" go mod edit -replace %s=%s\n", goInfo.ImportBasePath, relOut)
fmt.Println()
fmt.Println("You can then use the SDK in your Go code with:")
fmt.Println()
fmt.Printf(" import \"%s\"\n", goInfo.ImportBasePath)
fmt.Println()
return nil
}
// csharpPackageName converts a package name to a C#-friendly package name.
// for example "aws-api-gateway" becomes "AwsApiGateway".
func csharpPackageName(pkgName string) string {
title := cases.Title(language.English)
parts := strings.Split(pkgName, "-")
for i, part := range parts {
parts[i] = title.String(part)
}
return strings.Join(parts, "")
}
// Prints instructions for linking a locally generated SDK to an existing .NET
// project, in the absence of us attempting to perform this linking automatically.
func printDotnetLinkInstructions(root string, pkg *schema.Package, out string) error {
fmt.Printf("Successfully generated a .NET SDK for the %s package at %s\n", pkg.Name, out)
fmt.Println()
fmt.Println("To use this SDK in your .NET project, run the following command:")
fmt.Println()
relOut, err := filepath.Rel(root, out)
if err != nil {
return err
}
fmt.Printf(" dotnet add reference %s\n", filepath.Join(".", relOut))
fmt.Println()
fmt.Printf("You also need to add the following to your .csproj file of the program:\n")
fmt.Println()
fmt.Println(" <DefaultItemExcludes>$(DefaultItemExcludes);sdks/**/*.cs</DefaultItemExcludes>")
fmt.Println()
fmt.Println("You can then use the SDK in your .NET code with:")
fmt.Println()
fmt.Printf(" using Pulumi.%s;\n", csharpPackageName(pkg.Name))
fmt.Println()
return nil
}
// Prints instructions for linking a locally generated SDK to an existing Java
// project, in the absence of us attempting to perform this linking automatically.
func printJavaLinkInstructions(root string, pkg *schema.Package, out string) error {
fmt.Printf("Successfully generated a Java SDK for the %s package at %s\n", pkg.Name, out)
fmt.Println()
fmt.Println("To use this SDK in your Java project, complete the following steps:")
fmt.Println()
fmt.Println("1. Copy the contents of the generated SDK to your Java project:")
fmt.Printf(" cp -r %s/src/* %s/src\n", out, root)
fmt.Println()
fmt.Println("2. Add the SDK's dependencies to your Java project's build configuration.")
fmt.Println(" If you are using Maven, add the following dependencies to your pom.xml:")
fmt.Println()
fmt.Println(" <dependencies>")
fmt.Println(" <dependency>")
fmt.Println(" <groupId>com.google.code.findbugs</groupId>")
fmt.Println(" <artifactId>jsr305</artifactId>")
fmt.Println(" <version>3.0.2</version>")
fmt.Println(" </dependency>")
fmt.Println(" <dependency>")
fmt.Println(" <groupId>com.google.code.gson</groupId>")
fmt.Println(" <artifactId>gson</artifactId>")
fmt.Println(" <version>2.8.9</version>")
fmt.Println(" </dependency>")
fmt.Println(" </dependencies>")
fmt.Println()
return nil
}
// copyAll copies src to dst. If src is a directory, its contents will be copied
// recursively.
func copyAll(dst string, src string) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
// Recursively copy all files in a directory.
files, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("read dir: %w", err)
}
for _, file := range files {
name := file.Name()
copyerr := copyAll(filepath.Join(dst, name), filepath.Join(src, name))
if copyerr != nil {
return copyerr
}
}
} else if info.Mode().IsRegular() {
// Copy files by reading and rewriting their contents. Skip other special files.
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
dstdir := filepath.Dir(dst)
if err = os.MkdirAll(dstdir, 0o700); err != nil {
return err
}
if err = os.WriteFile(dst, data, info.Mode()); err != nil {
return err
}
}
return nil
}