mirror of https://github.com/pulumi/pulumi.git
1694 lines
52 KiB
Go
1694 lines
52 KiB
Go
// Copyright 2016-2019, 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 workspace
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/blang/semver"
|
|
"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/testing/diagtest"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/iotest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestLegacyPluginSelection_Prerelease(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.0")
|
|
v3 := semver.MustParse("0.3.0-alpha")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
result := LegacySelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", nil)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Equal(t, "0.2.0", result.Version.String())
|
|
}
|
|
|
|
func TestLegacyPluginSelection_PrereleaseRequested(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.0-alpha")
|
|
v3 := semver.MustParse("0.3.0-alpha")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
v := semver.MustParse("0.2.0")
|
|
result := LegacySelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", &v)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Equal(t, "0.3.0-alpha", result.Version.String())
|
|
}
|
|
|
|
func TestPluginSelection_ExactMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.0")
|
|
v3 := semver.MustParse("0.3.0")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Equal(t, "0.2.0", result.Version.String())
|
|
}
|
|
|
|
func TestPluginSelection_ExactMatchNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.1")
|
|
v3 := semver.MustParse("0.3.0")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestPluginSelection_PatchVersionSlide(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.0")
|
|
v21 := semver.MustParse("0.2.1")
|
|
v3 := semver.MustParse("0.3.0")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v21,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
requested := semver.MustParseRange(">=0.2.0 <0.3.0")
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Equal(t, "0.2.1", result.Version.String())
|
|
}
|
|
|
|
func TestPluginSelection_EmptyVersionNoAlternatives(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.1")
|
|
v3 := semver.MustParse("0.3.0")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: nil,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Nil(t, result.Version)
|
|
}
|
|
|
|
func TestPluginSelection_EmptyVersionWithAlternatives(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
v2 := semver.MustParse("0.2.0")
|
|
v3 := semver.MustParse("0.3.0")
|
|
candidatePlugins := []PluginInfo{
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v2,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: nil,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: nil,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "notmyplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v3,
|
|
},
|
|
{
|
|
Name: "myplugin",
|
|
Kind: apitype.AnalyzerPlugin,
|
|
Version: &v3,
|
|
},
|
|
}
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
assert.Equal(t, "0.2.0", result.Version.String())
|
|
}
|
|
|
|
func newMockReadCloser(data []byte) (io.ReadCloser, int64, error) {
|
|
return io.NopCloser(bytes.NewReader(data)), int64(len(data)), nil
|
|
}
|
|
|
|
func newMockReadCloserString(data string) (io.ReadCloser, int64, error) {
|
|
return newMockReadCloser([]byte(data))
|
|
}
|
|
|
|
//nolint:paralleltest // mutates environment variables
|
|
func TestPluginDownload(t *testing.T) {
|
|
expectedBytes := []byte{1, 2, 3}
|
|
token := "RaNd0m70K3n_"
|
|
|
|
t.Run("Pulumi GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", "")
|
|
version := semver.MustParse("4.30.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.30.0" {
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/654321",
|
|
"name": "pulumi-mockdl_4.30.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v4.30.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("get.pulumi.com", func(t *testing.T) {
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "otherdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
// Test that the asset isn't on github
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-otherdl/releases/tags/v4.32.0" {
|
|
return nil, -1, errors.New("404 not found")
|
|
}
|
|
assert.Equal(t,
|
|
"https://get.pulumi.com/releases/plugins/pulumi-resource-otherdl-v4.32.0-darwin-amd64.tar.gz",
|
|
req.URL.String())
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("Custom http URL", func(t *testing.T) {
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "http://customurl.jfrog.io/artifactory/pulumi-packages/package-name/v${VERSION}/${OS}/${ARCH}",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
assert.Equal(t,
|
|
"http://customurl.jfrog.io/artifactory/pulumi-packages/"+
|
|
"package-name/v4.32.0/darwin/amd64/pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz",
|
|
req.URL.String())
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("Custom https URL", func(t *testing.T) {
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "https://customurl.jfrog.io/artifactory/pulumi-packages/" +
|
|
"package-name/${NAME}/v${VERSION}/${OS}/${ARCH}/",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
assert.Equal(t,
|
|
"https://customurl.jfrog.io/artifactory/pulumi-packages/"+
|
|
"package-name/mockdl/v4.32.0/darwin/amd64/pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz",
|
|
req.URL.String())
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("Private Pulumi GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.32.0" {
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/654321",
|
|
"name": "pulumi-mockdl_4.32.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("Internal GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "github://api.git.org/ourorg/mock",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
// Test that the asset isn't on github
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.32.0" {
|
|
return nil, -1, errors.New("404 not found")
|
|
}
|
|
|
|
if req.URL.String() == "https://api.git.org/repos/ourorg/mock/releases/tags/v4.32.0" {
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.git.org/repos/ourorg/mock/releases/assets/654321",
|
|
"name": "pulumi-mockdl_4.32.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.git.org/repos/ourorg/mock/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.git.org/repos/ourorg/mock/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("Pulumi GitHub Releases With Checksum", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", "")
|
|
version := semver.MustParse("4.30.0")
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.30.0" {
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/654321",
|
|
"name": "pulumi-mockdl_4.30.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v4.30.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
|
|
chksum := "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81" //nolint:gosec
|
|
|
|
t.Run("Invalid Checksum", func(t *testing.T) {
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
Checksums: map[string][]byte{
|
|
"darwin-amd64": {0},
|
|
},
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
assert.Error(t, err, "invalid checksum, expected 00, actual "+chksum)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
|
|
t.Run("Valid Checksum", func(t *testing.T) {
|
|
checksum, err := hex.DecodeString(chksum)
|
|
assert.NoError(t, err)
|
|
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
Checksums: map[string][]byte{
|
|
"darwin-amd64": checksum,
|
|
},
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
|
|
t.Run("Missing Checksum", func(t *testing.T) {
|
|
// In this test the specification has checksums, but is missing the checksum for the current platform.
|
|
// There are two sensible ways to handle this:
|
|
// 1. Behave as if no checksums were specified at all, and simply fall back to not checking anything.
|
|
// 2. Error that the checksum for the current platform is missing.
|
|
// We choose to do the former, for now as that's more lenient.
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
Checksums: map[string][]byte{
|
|
"windows-amd64": {0},
|
|
},
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
})
|
|
t.Run("GitLab Releases", func(t *testing.T) {
|
|
t.Setenv("GITLAB_TOKEN", token)
|
|
version := semver.MustParse("1.23.4")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "gitlab://gitlab.com/278964",
|
|
Name: "mock-gitlab",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
assert.Equal(t,
|
|
"https://gitlab.com/api/v4/projects/278964/releases/v1.23.4/downloads/"+
|
|
"pulumi-resource-mock-gitlab-v1.23.4-windows-arm64.tar.gz", req.URL.String())
|
|
assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "windows", "arm64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("GitHub Releases with invalid token", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
version := semver.MustParse("4.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
attempts := 0
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
attempts++
|
|
|
|
if req.Header.Get("Authorization") == "token "+token {
|
|
// Fail with a 401 Unauthorized
|
|
return nil, -1, &downloadError{code: 401}
|
|
}
|
|
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.32.0" {
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/654321",
|
|
"name": "pulumi-mockdl_4.32.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3, attempts) // Failed attempt, then two successful attempts, first for the tag then the asset.
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
t.Run("GitHub Releases with disallowed", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
version := semver.MustParse("5.32.0")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mockdl",
|
|
Version: &version,
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
attempts := 0
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
attempts++
|
|
|
|
if req.Header.Get("Authorization") == "token "+token {
|
|
// Fail with a 403 Forbidden
|
|
return nil, -1, &downloadError{
|
|
code: 403,
|
|
header: http.Header{"x-ratelimit-remaining": []string{"42"}},
|
|
}
|
|
}
|
|
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v5.32.0" {
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"assets": [
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/654321",
|
|
"name": "pulumi-mockdl_5.32.0_checksums.txt"
|
|
},
|
|
{
|
|
"url": "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456",
|
|
"name": "pulumi-resource-mockdl-v5.32.0-darwin-amd64.tar.gz"
|
|
}
|
|
]
|
|
}
|
|
`)
|
|
}
|
|
|
|
assert.Equal(t, "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/assets/123456", req.URL.String())
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
return newMockReadCloser(expectedBytes)
|
|
}
|
|
r, l, err := source.Download(context.Background(), *spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
require.NoError(t, err)
|
|
readBytes, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3, attempts) // Failed attempt, then two successful attempts, first for the tag then the asset.
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
})
|
|
}
|
|
|
|
//nolint:paralleltest // mutates environment variables
|
|
func TestPluginGetLatestVersion(t *testing.T) {
|
|
token := "RaNd0m70K3n_"
|
|
|
|
t.Run("Pulumi GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", "")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mock-latest",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
expectedVersion := semver.MustParse("4.37.5")
|
|
source, err := spec.GetSource()
|
|
assert.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
assert.Equal(t,
|
|
"https://api.github.com/repos/pulumi/pulumi-mock-latest/releases/latest",
|
|
req.URL.String())
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"tag_name": "v4.37.5"
|
|
}`)
|
|
}
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedVersion, *version)
|
|
})
|
|
t.Run("Custom http URL", func(t *testing.T) {
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "http://customurl.jfrog.io/artifactory/pulumi-packages/package-name",
|
|
Name: "mock-latest",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
assert.Nil(t, version)
|
|
assert.EqualError(t, err, "GetLatestVersion is not supported for plugins from http sources")
|
|
})
|
|
t.Run("Custom https URL", func(t *testing.T) {
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "https://customurl.jfrog.io/artifactory/pulumi-packages/package-name",
|
|
Name: "mock-latest",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
assert.Nil(t, version)
|
|
assert.EqualError(t, err, "GetLatestVersion is not supported for plugins from http sources")
|
|
})
|
|
t.Run("Private Pulumi GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mock-private",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
expectedVersion := semver.MustParse("4.37.5")
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mock-private/releases/latest" {
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"tag_name": "v4.37.5"
|
|
}`)
|
|
}
|
|
|
|
panic("Unexpected call to getHTTPResponse")
|
|
}
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedVersion, *version)
|
|
})
|
|
t.Run("Internal GitHub Releases", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", token)
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "github://api.git.org/ourorg/mock",
|
|
Name: "mock-private",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
expectedVersion := semver.MustParse("4.37.5")
|
|
source, err := spec.GetSource()
|
|
assert.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://api.git.org/repos/ourorg/mock/releases/latest" {
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"tag_name": "v4.37.5"
|
|
}`)
|
|
}
|
|
|
|
panic("Unexpected call to getHTTPResponse")
|
|
}
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedVersion, *version)
|
|
})
|
|
t.Run("GitLab Releases", func(t *testing.T) {
|
|
t.Setenv("GITLAB_TOKEN", token)
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "gitlab://gitlab.com/278964",
|
|
Name: "mock-gitlab",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
expectedVersion := semver.MustParse("1.23.0")
|
|
source, err := spec.GetSource()
|
|
require.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
if req.URL.String() == "https://gitlab.com/api/v4/projects/278964/releases/permalink/latest" {
|
|
assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization"))
|
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
|
|
|
// Minimal JSON from the releases API to get the test to pass
|
|
return newMockReadCloserString(`{
|
|
"tag_name": "v1.23"
|
|
}`)
|
|
}
|
|
|
|
panic("Unexpected call to getHTTPResponse")
|
|
}
|
|
version, err := source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedVersion, *version)
|
|
})
|
|
t.Run("Hit GitHub ratelimit", func(t *testing.T) {
|
|
t.Setenv("GITHUB_TOKEN", "")
|
|
spec := PluginSpec{
|
|
PluginDownloadURL: "",
|
|
Name: "mock-latest",
|
|
Kind: apitype.PluginKind("resource"),
|
|
}
|
|
source, err := spec.GetSource()
|
|
assert.NoError(t, err)
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
return nil, 0, newDownloadError(403, req.URL, http.Header{"X-Ratelimit-Remaining": []string{"0"}})
|
|
}
|
|
_, err = source.GetLatestVersion(context.Background(), getHTTPResponse)
|
|
assert.ErrorContains(t, err, "rate limit exceeded")
|
|
assert.ErrorContains(t, err, "https://api.github.com/repos/pulumi/pulumi-mock-latest/releases/latest")
|
|
})
|
|
}
|
|
|
|
func TestParsePluginDownloadURLOverride(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type match struct {
|
|
name string
|
|
url string
|
|
ok bool
|
|
}
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected pluginDownloadOverrideArray
|
|
matches []match
|
|
expectedError string
|
|
}{
|
|
{
|
|
input: "",
|
|
expected: pluginDownloadOverrideArray{},
|
|
},
|
|
{
|
|
input: "^foo.*=https://foo",
|
|
expected: pluginDownloadOverrideArray{
|
|
{
|
|
reg: regexp.MustCompile("^foo.*"),
|
|
url: "https://foo",
|
|
},
|
|
},
|
|
matches: []match{
|
|
{
|
|
name: "foo",
|
|
url: "https://foo",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "foo-bar",
|
|
url: "https://foo",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "fo",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "nope",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: "^foo.*=https://foo,^bar.*=https://bar",
|
|
expected: pluginDownloadOverrideArray{
|
|
{
|
|
reg: regexp.MustCompile("^foo.*"),
|
|
url: "https://foo",
|
|
},
|
|
{
|
|
reg: regexp.MustCompile("^bar.*"),
|
|
url: "https://bar",
|
|
},
|
|
},
|
|
matches: []match{
|
|
{
|
|
name: "foo",
|
|
url: "https://foo",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "foo-bar",
|
|
url: "https://foo",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "fo",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "bar",
|
|
url: "https://bar",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "barbaz",
|
|
url: "https://bar",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "ba",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "nope",
|
|
url: "",
|
|
ok: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
input: "=", // missing regex and url
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"=\"",
|
|
},
|
|
{
|
|
input: "^foo.*=", // missing url
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"^foo.*=\"",
|
|
},
|
|
{
|
|
input: "=https://foo", // missing regex
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"=https://foo\"",
|
|
},
|
|
{
|
|
input: "^foo.*=https://foo,", // trailing comma
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"^foo.*=https://foo,\"",
|
|
},
|
|
{
|
|
input: "[=https://foo", // invalid regex
|
|
expectedError: "error parsing regexp: missing closing ]: `[`",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
actual, err := parsePluginDownloadURLOverrides(tt.input)
|
|
if tt.expectedError != "" {
|
|
assert.EqualError(t, err, tt.expectedError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
assert.Equal(t, tt.expected, actual)
|
|
|
|
if len(tt.matches) > 0 {
|
|
for _, match := range tt.matches {
|
|
actualURL, actualOK := actual.get(match.name)
|
|
assert.Equal(t, match.url, actualURL)
|
|
assert.Equal(t, match.ok, actualOK)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPluginDownloadOverrideArray_Get(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
overrides pluginDownloadOverrideArray
|
|
input string
|
|
expectedURL string
|
|
expectedMatch bool
|
|
}{
|
|
{
|
|
name: "No match",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`^test-plugin$`), url: "https://example.com/test-plugin"},
|
|
},
|
|
input: "another-plugin",
|
|
expectedURL: "",
|
|
expectedMatch: false,
|
|
},
|
|
{
|
|
name: "Simple match",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`^test-plugin$`), url: "https://example.com/test-plugin"},
|
|
},
|
|
input: "test-plugin",
|
|
expectedURL: "https://example.com/test-plugin",
|
|
expectedMatch: true,
|
|
},
|
|
{
|
|
name: "Match with name placeholders",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{
|
|
reg: regexp.MustCompile(`^(?P<org>[\w-]+)-v(?P<repo>\d+\.\d+\.\d+)$`),
|
|
url: "https://example.com/${org}/${repo}/plugin.zip",
|
|
},
|
|
},
|
|
input: "my-plugin-v1.2.3",
|
|
expectedURL: "https://example.com/my-plugin/1.2.3/plugin.zip",
|
|
expectedMatch: true,
|
|
},
|
|
{
|
|
name: "Match with index placeholders",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{
|
|
reg: regexp.MustCompile(`^(?P<org>[\w-]+)-v(?P<repo>\d+\.\d+\.\d+)$`),
|
|
url: "https://example.com/$1/$2/plugin.zip",
|
|
},
|
|
},
|
|
input: "my-plugin-v1.2.3",
|
|
expectedURL: "https://example.com/my-plugin/1.2.3/plugin.zip",
|
|
expectedMatch: true,
|
|
},
|
|
{
|
|
name: "Match with $0 placeholder",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`^.+$`), url: "https://example.com/downloads?source=$0"},
|
|
},
|
|
input: "test-plugin",
|
|
expectedURL: "https://example.com/downloads?source=test-plugin",
|
|
expectedMatch: true,
|
|
},
|
|
{
|
|
name: "Multiple overrides, second matches",
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`^test-plugin$`), url: "https://example.com/test-plugin"},
|
|
{reg: regexp.MustCompile(`^another-plugin$`), url: "https://example.com/another-plugin"},
|
|
},
|
|
input: "another-plugin",
|
|
expectedURL: "https://example.com/another-plugin",
|
|
expectedMatch: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
actualURL, actualMatch := tt.overrides.get(tt.input)
|
|
if actualURL != tt.expectedURL {
|
|
assert.Equal(t, tt.expectedURL, actualURL)
|
|
}
|
|
if actualMatch != tt.expectedMatch {
|
|
assert.Equal(t, tt.expectedMatch, actualMatch)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDownloadToFile_retries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Verifies that DownloadToFile retries on transient errors
|
|
// when trying to download plugins,
|
|
// and that it calls the wrapper and retry functions as expected.
|
|
//
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/12456.
|
|
|
|
var numRequests int
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodGet, r.Method, "expected GET request")
|
|
assert.Regexp(t, `/pulumi-language-myplugin-v1.0.0-\S+\.tar\.gz`, r.URL.Path,
|
|
"unexpected URL path")
|
|
|
|
// Fails all requests with a 500 error.
|
|
// This will cause every download attempt to fail
|
|
// and be retried.
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
numRequests++
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
defer func() {
|
|
assert.Equal(t, 5, numRequests,
|
|
"server received more requests than expected")
|
|
}()
|
|
|
|
// Create a fake plugin.
|
|
version := semver.MustParse("1.0.0")
|
|
spec := PluginSpec{
|
|
Name: "myplugin",
|
|
Kind: apitype.LanguagePlugin,
|
|
Version: &version,
|
|
PluginDownloadURL: server.URL,
|
|
PluginDir: t.TempDir(),
|
|
}
|
|
|
|
// numRetries is tracked separately from numRequests.
|
|
// numRequests is the number of requests received by the server,
|
|
// while numRetries is the number of times the retry function is called.
|
|
// These should match--the function is called on all failures.
|
|
var numRetries int
|
|
currentTime := time.Now()
|
|
_, err := (&pluginDownloader{
|
|
OnRetry: func(err error, attempt, limit int, delay time.Duration) {
|
|
assert.Equal(t, 5, limit, "unexpected retry limit")
|
|
numRetries++
|
|
assert.Equal(t, numRetries, attempt, "unexpected attempt number")
|
|
},
|
|
After: func(d time.Duration) <-chan time.Time {
|
|
currentTime = currentTime.Add(d)
|
|
ch := make(chan time.Time, 1)
|
|
ch <- currentTime
|
|
return ch
|
|
},
|
|
}).DownloadToFile(context.Background(), spec)
|
|
assert.ErrorContains(t, err, "failed to download plugin: myplugin-1.0.0")
|
|
assert.Equal(t, numRequests, numRetries)
|
|
}
|
|
|
|
//nolint:paralleltest // changes directory for process
|
|
func TestUnmarshalProjectWithProviderList(t *testing.T) {
|
|
t.Parallel()
|
|
tempdir := t.TempDir()
|
|
pyaml := filepath.Join(tempdir, "Pulumi.yaml")
|
|
|
|
// write to pyaml
|
|
err := os.WriteFile(pyaml, []byte(`name: test-yaml
|
|
runtime: yaml
|
|
description: "Test Pulumi YAML"
|
|
plugins:
|
|
providers:
|
|
- name: aws
|
|
version: 1.0.0
|
|
path: ../bin/aws`), 0o600)
|
|
assert.NoError(t, err)
|
|
|
|
proj, err := LoadProject(pyaml)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, proj.Plugins)
|
|
assert.Equal(t, 1, len(proj.Plugins.Providers))
|
|
assert.Equal(t, "aws", proj.Plugins.Providers[0].Name)
|
|
assert.Equal(t, "1.0.0", proj.Plugins.Providers[0].Version)
|
|
assert.Equal(t, "../bin/aws", proj.Plugins.Providers[0].Path)
|
|
}
|
|
|
|
//nolint:paralleltest // mutates pluginDownloadURLOverridesParsed
|
|
func TestPluginSpec_GetSource(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
spec PluginSpec
|
|
overrides pluginDownloadOverrideArray
|
|
expectedSourceType string
|
|
expectedURL string
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "Use PluginDownloadURL (HTTP)",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
PluginDownloadURL: "https://example.com/test-plugin",
|
|
},
|
|
expectedSourceType: "*workspace.httpSource",
|
|
expectedURL: "https://example.com/test-plugin",
|
|
},
|
|
{
|
|
name: "Use PluginDownloadURL (GitHub)",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
PluginDownloadURL: "github://api.github.com/owner/repo",
|
|
},
|
|
expectedSourceType: "*workspace.githubSource",
|
|
expectedURL: "github://api.github.com/owner/repo",
|
|
},
|
|
{
|
|
name: "Use PluginDownloadURL (GitLab)",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
PluginDownloadURL: "gitlab://mygitlab.example.com/proj1",
|
|
},
|
|
expectedSourceType: "*workspace.gitlabSource",
|
|
expectedURL: "gitlab://mygitlab.example.com/proj1",
|
|
},
|
|
{
|
|
name: "Use fallback source",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
},
|
|
expectedSourceType: "*workspace.fallbackSource",
|
|
expectedURL: "github://api.github.com/pulumi/pulumi-test-plugin",
|
|
},
|
|
{
|
|
name: "Apply override (HTTP)",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
},
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`test-plugin`), url: "https://example.com/test-plugin"},
|
|
},
|
|
expectedSourceType: "*workspace.httpSource",
|
|
expectedURL: "https://example.com/test-plugin",
|
|
},
|
|
{
|
|
name: "Apply override (GitHub)",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
},
|
|
overrides: pluginDownloadOverrideArray{
|
|
{reg: regexp.MustCompile(`test-plugin`), url: "github://api.github.com/test-org/test-plugin"},
|
|
},
|
|
expectedSourceType: "*workspace.githubSource",
|
|
expectedURL: "github://api.github.com/test-org/test-plugin",
|
|
},
|
|
{
|
|
name: "Apply checksums",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
Checksums: map[string][]byte{"checksum1": []byte("checksum2")},
|
|
},
|
|
expectedSourceType: "*workspace.checksumSource",
|
|
expectedURL: "github://api.github.com/pulumi/pulumi-test-plugin",
|
|
},
|
|
{
|
|
name: "Invalid URL",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
PluginDownloadURL: "://invalid-url",
|
|
},
|
|
expectedErrMsg: "parse \"://invalid-url\": missing protocol scheme",
|
|
},
|
|
{
|
|
name: "Unknown scheme",
|
|
spec: PluginSpec{
|
|
Name: "test-plugin",
|
|
Kind: apitype.PluginKind("resource"),
|
|
PluginDownloadURL: "unknown://example.com/plugin",
|
|
},
|
|
expectedErrMsg: "unknown plugin source scheme: unknown",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pluginDownloadURLOverridesParsed = tt.overrides
|
|
|
|
source, err := tt.spec.GetSource()
|
|
assert.Equal(t, tt.expectedErrMsg != "", err != nil)
|
|
if err != nil {
|
|
assert.Equal(t, tt.expectedErrMsg, err.Error())
|
|
return
|
|
}
|
|
actualSourceType := reflect.TypeOf(source).String()
|
|
assert.Equal(t, tt.expectedSourceType, actualSourceType)
|
|
assert.Equal(t, tt.expectedURL, source.URL())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMissingErrorText(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
tests := []struct {
|
|
Name string
|
|
Plugin PluginInfo
|
|
IncludeAmbient bool
|
|
ExpectedError string
|
|
}{
|
|
{
|
|
Name: "ResourceWithVersion",
|
|
Plugin: PluginInfo{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
IncludeAmbient: true,
|
|
ExpectedError: "no resource plugin 'pulumi-resource-myplugin' found in the workspace at version v0.1.0 " +
|
|
"or on your $PATH",
|
|
},
|
|
{
|
|
Name: "ResourceWithVersion_ExcludeAmbient",
|
|
Plugin: PluginInfo{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: &v1,
|
|
},
|
|
IncludeAmbient: false,
|
|
ExpectedError: "no resource plugin 'pulumi-resource-myplugin' found in the workspace at version v0.1.0",
|
|
},
|
|
{
|
|
Name: "ResourceWithoutVersion",
|
|
Plugin: PluginInfo{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: nil,
|
|
},
|
|
IncludeAmbient: true,
|
|
ExpectedError: "no resource plugin 'pulumi-resource-myplugin' found in the workspace or on your $PATH",
|
|
},
|
|
{
|
|
Name: "ResourceWithoutVersion_ExcludeAmbient",
|
|
Plugin: PluginInfo{
|
|
Name: "myplugin",
|
|
Kind: apitype.ResourcePlugin,
|
|
Version: nil,
|
|
},
|
|
IncludeAmbient: false,
|
|
ExpectedError: "no resource plugin 'pulumi-resource-myplugin' found in the workspace",
|
|
},
|
|
{
|
|
Name: "LanguageWithoutVersion",
|
|
Plugin: PluginInfo{
|
|
Name: "dotnet",
|
|
Kind: apitype.LanguagePlugin,
|
|
Version: nil,
|
|
},
|
|
IncludeAmbient: true,
|
|
ExpectedError: "no language plugin 'pulumi-language-dotnet' found in the workspace or on your $PATH",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := NewMissingError(tt.Plugin.Kind, tt.Plugin.Name, tt.Plugin.Version, tt.IncludeAmbient)
|
|
assert.EqualError(t, err, tt.ExpectedError)
|
|
})
|
|
}
|
|
}
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestBundledPluginSearch(t *testing.T) {
|
|
// Get the path of this executable
|
|
exe, err := os.Executable()
|
|
require.NoError(t, err)
|
|
|
|
// Create a fake side-by-side plugin next to this executable, it must match one of our bundled names
|
|
bundledPath := filepath.Join(filepath.Dir(exe), "pulumi-language-nodejs")
|
|
err = os.WriteFile(bundledPath, []byte{}, 0o700) //nolint: gosec // we intended to write an executable file here
|
|
require.NoError(t, err)
|
|
bundledPath, _ = filepath.EvalSymlinks(bundledPath)
|
|
t.Cleanup(func() {
|
|
err := os.Remove(bundledPath)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// Create another copy of the fake plugin in $PATH
|
|
pathDir := t.TempDir()
|
|
t.Setenv("PATH", pathDir)
|
|
ambientPath := filepath.Join(pathDir, "pulumi-language-nodejs")
|
|
err = os.WriteFile(ambientPath, []byte{}, 0o700) //nolint: gosec
|
|
require.NoError(t, err)
|
|
|
|
d := diagtest.LogSink(t)
|
|
|
|
// Lookup the plugin with ambient search turned on
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ambientPath, path)
|
|
|
|
// Lookup the plugin with ambient search turned off
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "true")
|
|
path, err = GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, bundledPath, path)
|
|
}
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestAmbientPluginsWarn(t *testing.T) {
|
|
// Create a fake plugin in the path
|
|
pathDir := t.TempDir()
|
|
t.Setenv("PATH", pathDir)
|
|
ambientPath := filepath.Join(pathDir, "pulumi-resource-mock")
|
|
err := os.WriteFile(ambientPath, []byte{}, 0o700) //nolint: gosec
|
|
require.NoError(t, err)
|
|
|
|
var stderr bytes.Buffer
|
|
d := diag.DefaultSink(
|
|
iotest.LogWriter(t), // stdout
|
|
&stderr,
|
|
diag.FormatOptions{Color: "never"},
|
|
)
|
|
|
|
// Lookup the plugin with ambient search turned on
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(d, apitype.ResourcePlugin, "mock", nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ambientPath, path)
|
|
|
|
// Check we get a warning about loading this plugin
|
|
expectedMessage := fmt.Sprintf("warning: using pulumi-resource-mock from $PATH at %s\n", ambientPath)
|
|
assert.Equal(t, expectedMessage, stderr.String())
|
|
}
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestBundledPluginsDoNotWarn(t *testing.T) {
|
|
// Get the path of this executable
|
|
exe, err := os.Executable()
|
|
require.NoError(t, err)
|
|
|
|
// Create a fake side-by-side plugin next to this executable, it must match one of our bundled names
|
|
bundledPath := filepath.Join(filepath.Dir(exe), "pulumi-language-nodejs")
|
|
err = os.WriteFile(bundledPath, []byte{}, 0o700) //nolint: gosec // we intended to write an executable file here
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := os.Remove(bundledPath)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// Add the executable directory to PATH
|
|
t.Setenv("PATH", filepath.Dir(exe))
|
|
|
|
var stderr bytes.Buffer
|
|
d := diag.DefaultSink(
|
|
iotest.LogWriter(t), // stdout
|
|
&stderr,
|
|
diag.FormatOptions{Color: "never"},
|
|
)
|
|
|
|
// Lookup the plugin with ambient search turned on
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, bundledPath, path)
|
|
|
|
// Check we don't get a warning about loading this plugin, because it's the bundled one _even_ though it's also on PATH
|
|
assert.Empty(t, stderr.String())
|
|
}
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/13656
|
|
//
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestSymlinkPathPluginsDoNotWarn(t *testing.T) {
|
|
// Get the path of this executable
|
|
exe, err := os.Executable()
|
|
require.NoError(t, err)
|
|
|
|
// Create a fake side-by-side plugin next to this executable, it must match one of our bundled names
|
|
bundledPath := filepath.Join(filepath.Dir(exe), "pulumi-language-nodejs")
|
|
err = os.WriteFile(bundledPath, []byte{}, 0o700) //nolint: gosec
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := os.Remove(bundledPath)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// Create a fake plugin in the path that is a symlink to the bundled plugin
|
|
pathDir := t.TempDir()
|
|
t.Setenv("PATH", pathDir)
|
|
ambientPath := filepath.Join(pathDir, "pulumi-language-nodejs")
|
|
err = os.Symlink(bundledPath, ambientPath)
|
|
require.NoError(t, err)
|
|
|
|
var stderr bytes.Buffer
|
|
d := diag.DefaultSink(
|
|
iotest.LogWriter(t), // stdout
|
|
&stderr,
|
|
diag.FormatOptions{Color: "never"},
|
|
)
|
|
|
|
// Lookup the plugin with ambient search turned on
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
|
require.NoError(t, err)
|
|
// We expect the ambient path to be returned, but not to warn because it resolves to the same file as the
|
|
// bundled path.
|
|
assert.Equal(t, ambientPath, path)
|
|
assert.Empty(t, stderr.String())
|
|
}
|
|
|
|
// Test that GetPluginInfo works against shimless plugins (i.e. those without a direct executable file).
|
|
//
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestPluginInfoShimless(t *testing.T) {
|
|
// Create a fake plugin in temp
|
|
pathDir := t.TempDir()
|
|
|
|
pluginPath := filepath.Join(pathDir, "pulumi-resource-mock")
|
|
err := os.MkdirAll(pluginPath, 0o700) //nolint: gosec
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(pluginPath, "PulumiPlugin.yaml"), []byte(`runtime: nodejs`), 0o600)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(pluginPath, "test.ts"), []byte(`testcode`), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
stat, err := os.Stat(pluginPath)
|
|
require.NoError(t, err)
|
|
|
|
var stderr bytes.Buffer
|
|
d := diag.DefaultSink(
|
|
iotest.LogWriter(t), // stdout
|
|
&stderr,
|
|
diag.FormatOptions{Color: "never"},
|
|
)
|
|
|
|
info, err := GetPluginInfo(d, apitype.ResourcePlugin, "mock", nil, []ProjectPlugin{
|
|
{
|
|
Name: "mock",
|
|
Kind: apitype.ResourcePlugin,
|
|
Path: pluginPath,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, pluginPath, info.Path)
|
|
assert.Equal(t, int64(23), info.Size)
|
|
assert.Equal(t, stat.ModTime(), info.InstallTime)
|
|
assert.Equal(t, stat.ModTime(), info.SchemaTime)
|
|
// schemaPaths are odd, they're one directory up from the plugin directory
|
|
assert.Equal(t, filepath.Join(filepath.Dir(pluginPath), "schema-mock.json"), info.SchemaPath)
|
|
}
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestProjectPluginsWithUncleanPath(t *testing.T) {
|
|
tempdir := t.TempDir()
|
|
|
|
err := os.WriteFile(filepath.Join(tempdir, "pulumi-resource-aws"), []byte{}, 0o600)
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(diagtest.LogSink(t), apitype.ResourcePlugin, "aws", nil, []ProjectPlugin{
|
|
{
|
|
Name: "aws",
|
|
Kind: apitype.ResourcePlugin,
|
|
Path: tempdir + "/", // path with a trailing slash
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, filepath.Join(tempdir, "pulumi-resource-aws"), path)
|
|
}
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
func TestProjectPluginsWithSymlink(t *testing.T) {
|
|
tempdir := t.TempDir()
|
|
|
|
err := os.Mkdir(filepath.Join(tempdir, "subdir"), 0o700)
|
|
require.NoError(t, err)
|
|
err = os.Symlink(filepath.Join(tempdir, "subdir"), filepath.Join(tempdir, "symlink"))
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(tempdir, "subdir", "pulumi-resource-aws"), []byte{}, 0o600)
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
|
path, err := GetPluginPath(diagtest.LogSink(t), apitype.ResourcePlugin, "aws", nil, []ProjectPlugin{
|
|
{
|
|
Name: "aws",
|
|
Kind: apitype.ResourcePlugin,
|
|
Path: filepath.Join(tempdir, "symlink"),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, filepath.Join(tempdir, "symlink", "pulumi-resource-aws"), path)
|
|
}
|