-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
mapper.go
165 lines (145 loc) 路 5.98 KB
/
mapper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// 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 convert
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/blang/semver"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
// An interface to map provider names (N.B. These aren't Pulumi provider names, but the names of "providers"
// in the source language being converted from) to plugin specific mapping data.
type Mapper interface {
// Returns plugin specific mapping data for the given provider name. Returns an empty result if no mapping
// information was available.
GetMapping(provider string) ([]byte, error)
}
type mapperPluginSpec struct {
name tokens.Package
version semver.Version
}
type pluginMapper struct {
host plugin.Host
conversion_key string
plugins []mapperPluginSpec
entries map[string][]byte
}
func NewPluginMapper(host plugin.Host, key string, mappings []string) (Mapper, error) {
entries := map[string][]byte{}
// Enumerate _all_ our installed plugins to ask for any mappings they provide. This allows users to
// convert aws terraform code for example by just having 'pulumi-aws' plugin locally, without needing to
// specify it anywhere on the command line, and without tf2pulumi needing to know about every possible
// plugin.
allPlugins, err := workspace.GetPlugins()
if err != nil {
return nil, fmt.Errorf("could not get plugins: %w", err)
}
// First assumption we only care about the latest version of each plugin. If we add support to get a
// mapping for plugin version 1, it seems unlikely that we would remove support for that mapping in v2, so
// the latest version should in most cases be fine. If a user case comes up where this is not fine we can
// provide the manual workaround that this is based on what is locally installed, not what is published
// and so the user can just delete the higher version plugins from their cache.
latestVersions := make(map[string]semver.Version)
for _, plugin := range allPlugins {
if plugin.Kind != workspace.ResourcePlugin {
continue
}
if cur, has := latestVersions[plugin.Name]; has {
if plugin.Version.GT(cur) {
latestVersions[plugin.Name] = *plugin.Version
}
} else {
latestVersions[plugin.Name] = *plugin.Version
}
}
// We now have a list of plugin specs (i.e. a name and version), save that list because we don't want to
// iterate all the plugins now because the convert might not even ask for any mappings.
plugins := make([]mapperPluginSpec, 0)
for pkg, version := range latestVersions {
plugins = append(plugins, mapperPluginSpec{
name: tokens.Package(pkg),
version: version,
})
}
// These take precedence over any plugin returned mappings, but we want to error early if we can't read
// any of these.
for _, path := range mappings {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read mapping file '%s': %w", path, err)
}
// Mapping file names are assumed to be the provider key.
provider := filepath.Base(path)
// strip the extension
dotIndex := strings.LastIndex(provider, ".")
if dotIndex != -1 {
provider = provider[0:dotIndex]
}
entries[provider] = data
}
return &pluginMapper{
host: host,
conversion_key: key,
plugins: plugins,
entries: entries,
}, nil
}
func (l *pluginMapper) GetMapping(provider string) ([]byte, error) {
// Do we already have an entry for this provider, if so use it
if entry, has := l.entries[provider]; has {
return entry, nil
}
// No entry yet, start popping providers off the plugin list and return the first one that returns
// conversion data for this provider for the given key we're looking for. Second assumption is that only
// one pulumi provider will provide a mapping for each source mapping. This _might_ change in the future
// if we for example add support to convert terraform to azure/aws-native, or multiple community members
// bridge the same terraform provider. But as above the decisions here are based on what's locally
// installed so the user can manually edit their plugin cache to be the set of plugins they want to use.
for {
if len(l.plugins) == 0 {
// No plugins left to look in, return that we don't have a mapping
return nil, nil
}
// Pop the first spec off the plugins list
pluginSpec := l.plugins[0]
l.plugins = l.plugins[1:]
providerPlugin, err := l.host.Provider(pluginSpec.name, &pluginSpec.version)
if err != nil {
// We should maybe be lenient here and ignore errors but for now assume it's better to fail out on
// things like providers failing to start.
return nil, fmt.Errorf("could not create provider '%s': %w", pluginSpec.name, err)
}
data, mappedProvider, err := providerPlugin.GetMapping(l.conversion_key)
if err != nil {
// This was an error calling GetMapping, not just that GetMapping returned a nil result. It's fine
// for GetMapping to return (nil, nil).
return nil, fmt.Errorf("could not get mapping for provider '%s': %w", pluginSpec.name, err)
}
// A provider returns empty if it didn't have a mapping
if mappedProvider != "" && len(data) != 0 {
// Don't overwrite entries, the first wins
if _, has := l.entries[mappedProvider]; !has {
l.entries[mappedProvider] = data
// If this was the provider we we're looking for we can now return it
if mappedProvider == provider {
return data, nil
}
}
}
}
}