mirror of https://github.com/pulumi/pulumi.git
317 lines
8.5 KiB
Go
317 lines
8.5 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 schema
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/natefinch/atomic"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/segmentio/encoding/json"
|
|
|
|
pkgWorkspace "github.com/pulumi/pulumi/pkg/v3/workspace"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
|
|
"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/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
)
|
|
|
|
type Loader interface {
|
|
LoadPackage(pkg string, version *semver.Version) (*Package, error)
|
|
}
|
|
|
|
type ReferenceLoader interface {
|
|
Loader
|
|
|
|
LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error)
|
|
}
|
|
|
|
type pluginLoader struct {
|
|
m sync.RWMutex
|
|
|
|
host plugin.Host
|
|
entries map[string]PackageReference
|
|
|
|
cacheOptions pluginLoaderCacheOptions
|
|
}
|
|
|
|
// Caching options intended for benchmarking or debugging:
|
|
type pluginLoaderCacheOptions struct {
|
|
// useEntriesCache enables in-memory re-use of packages
|
|
disableEntryCache bool
|
|
// useFileCache enables skipping plugin loading when possible and caching JSON schemas to files
|
|
disableFileCache bool
|
|
// useMmap enables the use of memory mapped IO to avoid copying the JSON schema
|
|
disableMmap bool
|
|
}
|
|
|
|
func NewPluginLoader(host plugin.Host) ReferenceLoader {
|
|
return &pluginLoader{
|
|
host: host,
|
|
entries: map[string]PackageReference{},
|
|
}
|
|
}
|
|
|
|
func newPluginLoaderWithOptions(host plugin.Host, cacheOptions pluginLoaderCacheOptions) ReferenceLoader {
|
|
return &pluginLoader{
|
|
host: host,
|
|
entries: map[string]PackageReference{},
|
|
|
|
cacheOptions: cacheOptions,
|
|
}
|
|
}
|
|
|
|
func (l *pluginLoader) getPackage(key string) (PackageReference, bool) {
|
|
if l.cacheOptions.disableEntryCache {
|
|
return nil, false
|
|
}
|
|
p, ok := l.entries[key]
|
|
return p, ok
|
|
}
|
|
|
|
func (l *pluginLoader) setPackage(key string, p PackageReference) PackageReference {
|
|
if l.cacheOptions.disableEntryCache {
|
|
return p
|
|
}
|
|
|
|
if p, ok := l.entries[key]; ok {
|
|
return p
|
|
}
|
|
|
|
l.entries[key] = p
|
|
return p
|
|
}
|
|
|
|
func (l *pluginLoader) LoadPackage(pkg string, version *semver.Version) (*Package, error) {
|
|
ref, err := l.LoadPackageReference(pkg, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ref.Definition()
|
|
}
|
|
|
|
var ErrGetSchemaNotImplemented = getSchemaNotImplemented{}
|
|
|
|
type getSchemaNotImplemented struct{}
|
|
|
|
func (f getSchemaNotImplemented) Error() string {
|
|
return "it looks like GetSchema is not implemented"
|
|
}
|
|
|
|
func schemaIsEmpty(schemaBytes []byte) bool {
|
|
// A non-empty schema is any that contains non-whitespace, non brace characters.
|
|
//
|
|
// Some providers implemented GetSchema initially by returning text matching the regular
|
|
// expression: "\s*\{\s*\}\s*". This handles those cases while not strictly checking that braces
|
|
// match or reading the whole document.
|
|
for _, v := range schemaBytes {
|
|
if v != ' ' && v != '\t' && v != '\r' && v != '\n' && v != '{' && v != '}' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (l *pluginLoader) LoadPackageReference(pkg string, version *semver.Version) (PackageReference, error) {
|
|
if pkg == "pulumi" {
|
|
return DefaultPulumiPackage.Reference(), nil
|
|
}
|
|
|
|
l.m.Lock()
|
|
defer l.m.Unlock()
|
|
|
|
key := packageIdentity(pkg, version)
|
|
if p, ok := l.getPackage(key); ok {
|
|
return p, nil
|
|
}
|
|
|
|
schemaBytes, version, err := l.loadSchemaBytes(context.TODO(), pkg, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if schemaIsEmpty(schemaBytes) {
|
|
return nil, getSchemaNotImplemented{}
|
|
}
|
|
|
|
var spec PartialPackageSpec
|
|
if _, err := json.Parse(schemaBytes, &spec, json.ZeroCopy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Insert a version into the spec if the package does not provide one or if the
|
|
// existing version is less than the provided one
|
|
if version != nil {
|
|
setVersion := true
|
|
if spec.PackageInfoSpec.Version != "" {
|
|
vSemver, err := semver.Make(spec.PackageInfoSpec.Version)
|
|
if err == nil {
|
|
if vSemver.Compare(*version) == 1 {
|
|
setVersion = false
|
|
}
|
|
}
|
|
}
|
|
if setVersion {
|
|
spec.PackageInfoSpec.Version = version.String()
|
|
}
|
|
}
|
|
|
|
p, err := ImportPartialSpec(spec, nil, l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l.setPackage(key, p), nil
|
|
}
|
|
|
|
func LoadPackageReference(loader Loader, pkg string, version *semver.Version) (PackageReference, error) {
|
|
var ref PackageReference
|
|
var err error
|
|
if refLoader, ok := loader.(ReferenceLoader); ok {
|
|
ref, err = refLoader.LoadPackageReference(pkg, version)
|
|
} else {
|
|
p, pErr := loader.LoadPackage(pkg, version)
|
|
err = pErr
|
|
if err == nil {
|
|
ref = p.Reference()
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if pkg != ref.Name() || version != nil && ref.Version() != nil && !ref.Version().Equals(*version) {
|
|
if l, ok := loader.(*pluginLoader); ok {
|
|
return nil, fmt.Errorf("req: %s@%v: entries: %v (returned %s@%v)", pkg, version,
|
|
l.entries, ref.Name(), ref.Version())
|
|
}
|
|
return nil, fmt.Errorf("loader returned %s@%v: expected %s@%v", ref.Name(), ref.Version(), pkg, version)
|
|
}
|
|
|
|
return ref, nil
|
|
}
|
|
|
|
func (l *pluginLoader) loadSchemaBytes(
|
|
ctx context.Context, pkg string, version *semver.Version,
|
|
) ([]byte, *semver.Version, error) {
|
|
attachPort, err := plugin.GetProviderAttachPort(tokens.Package(pkg))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// If PULUMI_DEBUG_PROVIDERS requested an attach port, skip caching and workspace
|
|
// interaction and load the schema directly from the given port.
|
|
if attachPort != nil {
|
|
schemaBytes, provider, err := l.loadPluginSchemaBytes(ctx, pkg, version)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Error loading schema from plugin: %w", err)
|
|
}
|
|
|
|
if version == nil {
|
|
info, err := provider.GetPluginInfo(ctx)
|
|
contract.IgnoreError(err) // nonfatal error
|
|
version = info.Version
|
|
}
|
|
return schemaBytes, version, nil
|
|
}
|
|
|
|
pluginInfo, err := l.host.ResolvePlugin(apitype.ResourcePlugin, pkg, version)
|
|
if err != nil {
|
|
// Try and install the plugin if it was missing and try again, unless auto plugin installs are turned off.
|
|
var missingError *workspace.MissingError
|
|
if !errors.As(err, &missingError) || env.DisableAutomaticPluginAcquisition.Value() {
|
|
return nil, nil, err
|
|
}
|
|
|
|
spec := workspace.PluginSpec{
|
|
Kind: apitype.ResourcePlugin,
|
|
Name: pkg,
|
|
Version: version,
|
|
}
|
|
|
|
log := func(sev diag.Severity, msg string) {
|
|
l.host.Log(sev, "", msg, 0)
|
|
}
|
|
|
|
_, err = pkgWorkspace.InstallPlugin(spec, log)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
pluginInfo, err = l.host.ResolvePlugin(apitype.ResourcePlugin, pkg, version)
|
|
if err != nil {
|
|
return nil, version, err
|
|
}
|
|
}
|
|
contract.Assertf(pluginInfo != nil, "loading pkg %q: pluginInfo was unexpectedly nil", pkg)
|
|
|
|
if version == nil {
|
|
version = pluginInfo.Version
|
|
}
|
|
|
|
if pluginInfo.SchemaPath != "" && version != nil {
|
|
schemaBytes, ok := l.loadCachedSchemaBytes(pkg, pluginInfo.SchemaPath, pluginInfo.SchemaTime)
|
|
if ok {
|
|
return schemaBytes, nil, nil
|
|
}
|
|
}
|
|
|
|
schemaBytes, provider, err := l.loadPluginSchemaBytes(ctx, pkg, version)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Error loading schema from plugin: %w", err)
|
|
}
|
|
|
|
if pluginInfo.SchemaPath != "" {
|
|
err = atomic.WriteFile(pluginInfo.SchemaPath, bytes.NewReader(schemaBytes))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Error writing schema from plugin to cache: %w", err)
|
|
}
|
|
}
|
|
|
|
if version == nil {
|
|
info, _ := provider.GetPluginInfo(ctx) // nonfatal error
|
|
version = info.Version
|
|
}
|
|
|
|
return schemaBytes, version, nil
|
|
}
|
|
|
|
func (l *pluginLoader) loadPluginSchemaBytes(
|
|
ctx context.Context, pkg string, version *semver.Version,
|
|
) ([]byte, plugin.Provider, error) {
|
|
provider, err := l.host.Provider(tokens.Package(pkg), version)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
contract.Assertf(provider != nil, "unexpected nil provider for %s@%v", pkg, version)
|
|
|
|
schemaFormatVersion := 0
|
|
schema, err := provider.GetSchema(ctx, plugin.GetSchemaRequest{
|
|
Version: schemaFormatVersion,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return schema.Schema, provider, nil
|
|
}
|