Skip to content

Commit

Permalink
Addd Eject function to use from "pulumi convert" (#630)
Browse files Browse the repository at this point in the history
  • Loading branch information
Frassle committed Nov 4, 2022
1 parent fe608ee commit 330791e
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 175 deletions.
76 changes: 24 additions & 52 deletions pkg/tf2pulumi/convert/convert.go
Expand Up @@ -15,7 +15,6 @@
package convert

import (
"bytes"
"io"
"log"
"os"
Expand All @@ -34,7 +33,6 @@ import (
hcl2python "github.com/pulumi/pulumi/pkg/v3/codegen/python"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

const (
Expand Down Expand Up @@ -75,59 +73,40 @@ func Convert(opts Options) (map[string][]byte, Diagnostics, error) {
opts.ProviderInfoSource = il.PluginProviderInfoSource
}

// Attempt to load the config as TF11 first. If this succeeds, use TF11 semantics unless either the config
// or the options specify otherwise.
generatedFiles, useTF12, tf11Err := convertTF11(opts)
if !useTF12 {
if tf11Err != nil {
return nil, Diagnostics{}, tf11Err
}
return generatedFiles, Diagnostics{}, nil
}

var tf12Files []*syntax.File
var diagnostics hcl.Diagnostics

if tf11Err == nil {
// Parse the config.
parser := syntax.NewParser()
for filename, contents := range generatedFiles {
err := parser.ParseFile(bytes.NewReader(contents), filename)
contract.Assert(err == nil)
}
if parser.Diagnostics.HasErrors() {
return nil, Diagnostics{All: parser.Diagnostics, files: parser.Files}, nil
}
tf12Files, diagnostics = parser.Files, append(diagnostics, parser.Diagnostics...)
} else {
files, diags := parseTF12(opts)
if !diags.HasErrors() {
tf12Files, diagnostics = files, append(diagnostics, diags...)
} else if opts.TerraformVersion != "11" {
return nil, Diagnostics{All: diags, files: files}, nil
} else {
return nil, Diagnostics{}, tf11Err
}
ejectOpts := EjectOptions{
AllowMissingProperties: opts.AllowMissingProperties,
AllowMissingProviders: opts.AllowMissingProviders,
AllowMissingVariables: opts.AllowMissingVariables,
AllowMissingComments: opts.AllowMissingComments,
AnnotateNodesWithLocations: opts.AnnotateNodesWithLocations,
FilterResourceNames: opts.FilterResourceNames,
ResourceNameProperty: opts.ResourceNameProperty,
Root: opts.Root,
PackageCache: opts.PackageCache,
PluginHost: opts.PluginHost,
Loader: opts.Loader,
ProviderInfoSource: opts.ProviderInfoSource,
Logger: opts.Logger,
SkipResourceTypechecking: opts.SkipResourceTypechecking,
TargetSDKVersion: opts.TargetSDKVersion,
TerraformVersion: opts.TerraformVersion,
}

tf12Files, program, programDiags, err := convertTF12(tf12Files, opts)
if err != nil {
return nil, Diagnostics{}, err
}
tfFiles, program, diagnostics, err := internalEject(ejectOpts)

diagnostics = append(diagnostics, programDiags...)
if diagnostics.HasErrors() {
return nil, Diagnostics{All: diagnostics, files: tf12Files}, nil
return nil, Diagnostics{All: diagnostics, files: tfFiles}, nil
}

var genDiags hcl.Diagnostics
var generatedFiles map[string][]byte
switch opts.TargetLanguage {
case LanguageTypescript:
generatedFiles, genDiags, err = hcl2nodejs.GenerateProgram(program)
diagnostics = append(diagnostics, genDiags...)
case LanguagePulumi:
generatedFiles = map[string][]byte{}
for _, f := range tf12Files {
for _, f := range tfFiles {
generatedFiles[f.Name] = f.Bytes
}
case LanguagePython:
Expand All @@ -147,13 +126,13 @@ func Convert(opts Options) (map[string][]byte, Diagnostics, error) {
diagnostics = append(diagnostics, genDiags...)
}
if err != nil {
return nil, Diagnostics{All: diagnostics, files: tf12Files}, err
return nil, Diagnostics{All: diagnostics, files: tfFiles}, err
}
if diagnostics.HasErrors() {
return nil, Diagnostics{All: diagnostics, files: tf12Files}, nil
return nil, Diagnostics{All: diagnostics, files: tfFiles}, nil
}

return generatedFiles, Diagnostics{All: diagnostics, files: tf12Files}, nil
return generatedFiles, Diagnostics{All: diagnostics, files: tfFiles}, nil
}

type Options struct {
Expand Down Expand Up @@ -200,10 +179,3 @@ type Options struct {
// TargetOptions captures any target-specific options.
TargetOptions interface{}
}

// logf writes a formatted message to the configured logger, if any.
func (o Options) logf(format string, arguments ...interface{}) {
if o.Logger != nil {
o.Logger.Printf(format, arguments...)
}
}
155 changes: 155 additions & 0 deletions pkg/tf2pulumi/convert/eject.go
@@ -0,0 +1,155 @@
// Copyright 2016-2018, 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 convert

import (
"bytes"
"fmt"
"log"
"os"

"github.com/hashicorp/hcl/v2"
"github.com/spf13/afero"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tf2pulumi/il"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

type EjectOptions struct {
// AllowMissingProperties, if true, allows code-gen to continue even if the input configuration does not include.
// values for required properties.
AllowMissingProperties bool
// AllowMissingProviders, if true, allows code-gen to continue even if resource providers are missing.
AllowMissingProviders bool
// AllowMissingVariables, if true, allows code-gen to continue even if the input configuration references missing
// variables.
AllowMissingVariables bool
// AllowMissingComments allows binding to succeed even if there are errors extracting comments from the source.
AllowMissingComments bool
// AnnotateNodesWithLocations is true if the generated source code should contain comments that annotate top-level
// nodes with their original source locations.
AnnotateNodesWithLocations bool
// FilterResourceNames, if true, removes the property indicated by ResourceNameProperty from all resources in the
// graph.
FilterResourceNames bool
// ResourceNameProperty sets the key of the resource name property that will be removed if FilterResourceNames is
// true.
ResourceNameProperty string
// Root, when set, overrides the default filesystem used to load the source Terraform module.
Root afero.Fs
// Optional package cache.
PackageCache *pcl.PackageCache
// Optional plugin host.
PluginHost plugin.Host
// Optional Loader.
Loader schema.Loader
// Optional source for provider schema information.
ProviderInfoSource il.ProviderInfoSource
// Optional logger for diagnostic information.
Logger *log.Logger
// SkipResourceTypechecking, if true, allows code-gen to continue even if resource inputs fail to typecheck.
SkipResourceTypechecking bool
// The target SDK version.
TargetSDKVersion string
// The version of Terraform targeteds by the input configuration.
TerraformVersion string
}

// logf writes a formatted message to the configured logger, if any.
func (o EjectOptions) logf(format string, arguments ...interface{}) {
if o.Logger != nil {
o.Logger.Printf(format, arguments...)
}
}

// Eject converts a Terraform module at the provided location into a Pulumi module.
func Eject(dir string, loader schema.ReferenceLoader) (*workspace.Project, *pcl.Program, error) {
if loader == nil {
panic("must provide a non-nil loader")
}

opts := EjectOptions{
Root: afero.NewBasePathFs(afero.NewOsFs(), dir),
Loader: loader,
}

tfFiles, program, diags, err := internalEject(opts)

d := Diagnostics{All: diags, files: tfFiles}
diagWriter := d.NewDiagnosticWriter(os.Stderr, 0, true)
if len(diags) != 0 {
err := diagWriter.WriteDiagnostics(diags)
if err != nil {
return nil, nil, err
}
}
if err != nil {
return nil, nil, fmt.Errorf("failed to load Terraform configuration, %v", err)
}

project := &workspace.Project{
Name: "tf2pulumi",
}

return project, program, nil
}

func internalEject(opts EjectOptions) ([]*syntax.File, *pcl.Program, hcl.Diagnostics, error) {
// Set default options where appropriate.
if opts.ProviderInfoSource == nil {
opts.ProviderInfoSource = il.PluginProviderInfoSource
}

// Attempt to load the config as TF11 first. If this succeeds, use TF11 semantics unless either the config
// or the options specify otherwise.
generatedFiles, tf11Err := convertTF11(opts)

var tf12Files []*syntax.File
var diagnostics hcl.Diagnostics

if tf11Err == nil {
// Parse the config.
parser := syntax.NewParser()
for filename, contents := range generatedFiles {
err := parser.ParseFile(bytes.NewReader(contents), filename)
contract.Assert(err == nil)
}
tf12Files, diagnostics = parser.Files, append(diagnostics, parser.Diagnostics...)
if diagnostics.HasErrors() {
return tf12Files, nil, diagnostics, nil
}
} else {
tf12Files, diagnostics = parseTF12(opts)
if diagnostics.HasErrors() {
return tf12Files, nil, diagnostics, nil
}
}

tf12Files, program, programDiags, err := convertTF12(tf12Files, opts)
if err != nil {
return nil, nil, nil, err
}

diagnostics = append(diagnostics, programDiags...)
if diagnostics.HasErrors() {
return tf12Files, nil, diagnostics, nil
}
return tf12Files, program, diagnostics, nil
}

0 comments on commit 330791e

Please sign in to comment.