2019-03-01 23:42:38 +00:00
|
|
|
// 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 (
|
2023-02-11 11:05:06 +00:00
|
|
|
"bytes"
|
2022-08-30 09:44:56 +00:00
|
|
|
"encoding/hex"
|
2022-03-03 09:22:21 +00:00
|
|
|
"errors"
|
2022-02-17 21:20:29 +00:00
|
|
|
"fmt"
|
2022-03-03 09:22:21 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
2023-03-20 19:58:59 +00:00
|
|
|
"net/http/httptest"
|
2023-01-06 22:39:16 +00:00
|
|
|
"os"
|
2022-07-22 13:17:43 +00:00
|
|
|
"path/filepath"
|
URL-based plugin source overrides via env var (#16648)
### Motivation
Pulumi plugin binaries can be downloaded by the CLI from multiple
sources. By default, it's downloaded from Pulumi's GitHub releases or
get.pulumi.com, but plugins can also specify their binary sources via
the `PluginDownloadURL` schema option. They can point to custom GitHub,
Gitlab, or HTTP locations.
Enterprise customers ask for a way to isolate the CLI from downloads
from random locations and to configure the CLI to go to their internal
pre-approved artefact location instead. This way, Pulumi can run in
"air-gapped" environments (which still have access to Cloud APIs, of
course).
Related issues:
- https://github.com/pulumi/pulumi/issues/14459
- https://github.com/pulumi/pulumi/issues/16240
Currently, there is a basic mechanism to do so via the variable
`pluginDownloadURLOverrides`, but it has two major limitations:
- The variable value is set via a compile-time flag, so it requires a
custom build of the CLI
- The overrides are based on the plugin name, so the rules must be
defined without access to the original URL, which makes it hard to
provide universal rules and still distinguish between first-party,
public third-party, or private in-house plugins
- We ignore overrides for all plugins that have `PluginDownloadURL` set
- Overrides can set a plugin replacement redirect only to HTTP(s)
addresses
### Proposal
This PR makes two sets of changes:
1. It allows passing overrides via the
`PULUMI_PLUGIN_DOWNLOAD_URL_OVERRIDES` environment variable. The
compile-time flag is still supported, but the env var takes priority.
More configuration levers could be supported, but it not clear if we
have good ones until [Support .pulumirc file for global
config](https://github.com/pulumi/pulumi/issues/13484) is implemented. I
don't expect users to want to set this via their stack configs, but I'm
curious what others think. In any case, more sources can be added later.
2. The overrides now apply based on the original download URL, not just
on plugin names. Actually, it's the base URL of a download source that
is passed to the regexp matcher. Examples of possible options are:
- `github://api.github.com/pulumi/pulumi-xyz` for a first-party plugin
(note that we don't pass `get.pulumi.com`
- `github://api.github.com/pulumiverse/pulumi-grafana` for a community
plugin that sets `PluginDownloadURL`
- `gitlab://gitlab-host/proj-name` for a community plugin hosted on
Gitlab
- `https://example.com/downloads/` for HTTP sources
So, the override
`^github://api.github.com/pulumi/pulumi-xyz=https://example.com/downloads/pulumi-xyz/`
will override the single provider URL from our GitHub releases to the
given HTTP location.
On top of that, regular expressions may contain name groups to capture
and use templated values. For example,
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/${org}/${repo}`
captures any GitHub plugin and redirects it to its corresponding HTTP
location. Group indices are also supported: the above override can also
be written as
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/$1/$2`,
with `$0` meaning the full match.
The override URLs have the same semantics as `PluginDownloadURL`, so
they can point to GitHub, Gitlab, HTTP, or anything we introduce in the
future.
### Impact
Technically, this is a breaking change, because name-based overrides
will stop working. However, we are fairly certain that we have a single
customer using the existing compile-time approach, and they indicated
that they don't need the name-based overrides if they have URL-based
overrides. I reviewed this PR with them and made sure they can migrate
immediately after the change is released.
Backwards compatibility is slightly tricky, because we'd need to keep
name-based override _and_ not applying them to third-party plugins. But
we can do it if necessary.
Resolve #16240
2024-07-26 10:37:09 +00:00
|
|
|
"reflect"
|
2022-01-21 17:04:10 +00:00
|
|
|
"regexp"
|
2019-03-01 23:42:38 +00:00
|
|
|
"testing"
|
2023-03-20 19:58:59 +00:00
|
|
|
"time"
|
2019-03-01 23:42:38 +00:00
|
|
|
|
|
|
|
"github.com/blang/semver"
|
2024-04-25 17:30:30 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
2023-08-07 12:15:57 +00:00
|
|
|
"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"
|
2019-03-01 23:42:38 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2023-02-11 11:05:06 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2019-03-01 23:42:38 +00:00
|
|
|
)
|
|
|
|
|
2023-12-03 09:15:07 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-04-25 17:30:30 +00:00
|
|
|
result := LegacySelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", nil)
|
2023-12-03 09:15:07 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2023-12-03 09:15:07 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
v := semver.MustParse("0.2.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := LegacySelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", &v)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.NotNil(t, result)
|
|
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
|
|
assert.Equal(t, "0.3.0-alpha", result.Version.String())
|
|
|
|
}
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
func TestPluginSelection_ExactMatch(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
|
|
v2 := semver.MustParse("0.2.0")
|
|
|
|
v3 := semver.MustParse("0.3.0")
|
|
|
|
candidatePlugins := []PluginInfo{
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.NotNil(t, result)
|
2019-03-01 23:42:38 +00:00
|
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
|
|
assert.Equal(t, "0.2.0", result.Version.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginSelection_ExactMatchNotFound(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
|
|
v2 := semver.MustParse("0.2.1")
|
|
|
|
v3 := semver.MustParse("0.3.0")
|
|
|
|
candidatePlugins := []PluginInfo{
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.Nil(t, result)
|
2019-03-01 23:42:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginSelection_PatchVersionSlide(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v21,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
requested := semver.MustParseRange(">=0.2.0 <0.3.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.NotNil(t, result)
|
2019-03-01 23:42:38 +00:00
|
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
|
|
assert.Equal(t, "0.2.1", result.Version.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginSelection_EmptyVersionNoAlternatives(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
|
|
v2 := semver.MustParse("0.2.1")
|
|
|
|
v3 := semver.MustParse("0.3.0")
|
|
|
|
candidatePlugins := []PluginInfo{
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.NotNil(t, result)
|
2019-03-01 23:42:38 +00:00
|
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
|
|
assert.Nil(t, result.Version)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginSelection_EmptyVersionWithAlternatives(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-03-01 23:42:38 +00:00
|
|
|
v1 := semver.MustParse("0.1.0")
|
|
|
|
v2 := semver.MustParse("0.2.0")
|
|
|
|
v3 := semver.MustParse("0.3.0")
|
|
|
|
candidatePlugins := []PluginInfo{
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "notmyplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "myplugin",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.AnalyzerPlugin,
|
2019-03-01 23:42:38 +00:00
|
|
|
Version: &v3,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
requested := semver.MustParseRange("0.2.0")
|
2024-04-25 17:30:30 +00:00
|
|
|
result := SelectCompatiblePlugin(candidatePlugins, apitype.ResourcePlugin, "myplugin", requested)
|
2023-12-03 09:15:07 +00:00
|
|
|
assert.NotNil(t, result)
|
2019-03-01 23:42:38 +00:00
|
|
|
assert.Equal(t, "myplugin", result.Name)
|
|
|
|
assert.Equal(t, "0.2.0", result.Version.String())
|
|
|
|
}
|
2021-11-29 23:21:55 +00:00
|
|
|
|
2022-03-03 09:22:21 +00:00
|
|
|
func newMockReadCloser(data []byte) (io.ReadCloser, int64, error) {
|
2023-02-11 11:05:06 +00:00
|
|
|
return io.NopCloser(bytes.NewReader(data)), int64(len(data)), nil
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newMockReadCloserString(data string) (io.ReadCloser, int64, error) {
|
2023-02-11 11:05:06 +00:00
|
|
|
return newMockReadCloser([]byte(data))
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-04 08:17:41 +00:00
|
|
|
//nolint:paralleltest // mutates environment variables
|
2022-03-03 09:22:21 +00:00
|
|
|
func TestPluginDownload(t *testing.T) {
|
2022-03-14 21:14:36 +00:00
|
|
|
expectedBytes := []byte{1, 2, 3}
|
|
|
|
token := "RaNd0m70K3n_"
|
|
|
|
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Pulumi GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", "")
|
2022-06-29 19:15:01 +00:00
|
|
|
version := semver.MustParse("4.30.0")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-01-20 15:50:11 +00:00
|
|
|
PluginDownloadURL: "",
|
2022-03-04 08:17:41 +00:00
|
|
|
Name: "mockdl",
|
2022-01-20 15:50:11 +00:00
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-01-20 15:50:11 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
2022-06-29 19:15:01 +00:00
|
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mockdl/releases/tags/v4.30.0" {
|
2023-03-09 09:36:55 +00:00
|
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
2022-06-29 19:15:01 +00:00
|
|
|
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"))
|
2022-03-14 21:14:36 +00:00
|
|
|
return newMockReadCloser(expectedBytes)
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
2022-01-20 15:50:11 +00:00
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("get.pulumi.com", func(t *testing.T) {
|
2022-01-20 15:50:11 +00:00
|
|
|
version := semver.MustParse("4.32.0")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-01-20 15:50:11 +00:00
|
|
|
PluginDownloadURL: "",
|
2022-06-29 19:15:01 +00:00
|
|
|
Name: "otherdl",
|
2022-01-20 15:50:11 +00:00
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-01-20 15:50:11 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
|
|
// Test that the asset isn't on github
|
2022-06-29 19:15:01 +00:00
|
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-otherdl/releases/tags/v4.32.0" {
|
2022-03-03 09:22:21 +00:00
|
|
|
return nil, -1, errors.New("404 not found")
|
|
|
|
}
|
|
|
|
assert.Equal(t,
|
2022-06-29 19:15:01 +00:00
|
|
|
"https://get.pulumi.com/releases/plugins/pulumi-resource-otherdl-v4.32.0-darwin-amd64.tar.gz",
|
2022-03-03 09:22:21 +00:00
|
|
|
req.URL.String())
|
2022-03-14 21:14:36 +00:00
|
|
|
return newMockReadCloser(expectedBytes)
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
2022-01-20 15:50:11 +00:00
|
|
|
})
|
2023-02-15 19:48:02 +00:00
|
|
|
t.Run("Custom http URL", func(t *testing.T) {
|
|
|
|
version := semver.MustParse("4.32.0")
|
|
|
|
spec := PluginSpec{
|
2023-07-10 09:16:51 +00:00
|
|
|
PluginDownloadURL: "http://customurl.jfrog.io/artifactory/pulumi-packages/package-name/v${VERSION}/${OS}/${ARCH}",
|
2023-02-15 19:48:02 +00:00
|
|
|
Name: "mockdl",
|
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-02-15 19:48:02 +00:00
|
|
|
}
|
|
|
|
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/"+
|
2023-07-10 09:16:51 +00:00
|
|
|
"package-name/v4.32.0/darwin/amd64/pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz",
|
2023-02-15 19:48:02 +00:00
|
|
|
req.URL.String())
|
|
|
|
return newMockReadCloser(expectedBytes)
|
|
|
|
}
|
|
|
|
r, l, err := source.Download(*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) {
|
2022-01-20 15:50:11 +00:00
|
|
|
version := semver.MustParse("4.32.0")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2023-10-30 09:29:12 +00:00
|
|
|
PluginDownloadURL: "https://customurl.jfrog.io/artifactory/pulumi-packages/" +
|
|
|
|
"package-name/${NAME}/v${VERSION}/${OS}/${ARCH}/",
|
|
|
|
Name: "mockdl",
|
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-01-20 15:50:11 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
|
|
assert.Equal(t,
|
|
|
|
"https://customurl.jfrog.io/artifactory/pulumi-packages/"+
|
2023-10-30 09:29:12 +00:00
|
|
|
"package-name/mockdl/v4.32.0/darwin/amd64/pulumi-resource-mockdl-v4.32.0-darwin-amd64.tar.gz",
|
2022-03-03 09:22:21 +00:00
|
|
|
req.URL.String())
|
2022-03-14 21:14:36 +00:00
|
|
|
return newMockReadCloser(expectedBytes)
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
2022-01-20 15:50:11 +00:00
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Private Pulumi GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", token)
|
2022-03-14 21:14:36 +00:00
|
|
|
version := semver.MustParse("4.32.0")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-03-14 21:14:36 +00:00
|
|
|
PluginDownloadURL: "",
|
|
|
|
Name: "mockdl",
|
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-03-14 21:14:36 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
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" {
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-03-14 21:14:36 +00:00
|
|
|
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())
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-03-03 09:22:21 +00:00
|
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
2022-03-14 21:14:36 +00:00
|
|
|
return newMockReadCloser(expectedBytes)
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
2022-03-03 09:22:21 +00:00
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Internal GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", token)
|
2022-06-29 19:15:01 +00:00
|
|
|
version := semver.MustParse("4.32.0")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-06-29 19:15:01 +00:00
|
|
|
PluginDownloadURL: "github://api.git.org/ourorg/mock",
|
|
|
|
Name: "mockdl",
|
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-06-29 19:15:01 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-06-29 19:15:01 +00:00
|
|
|
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" {
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-06-29 19:15:01 +00:00
|
|
|
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())
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-06-29 19:15:01 +00:00
|
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
|
|
return newMockReadCloser(expectedBytes)
|
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-06-29 19:15:01 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Pulumi GitHub Releases With Checksum", func(t *testing.T) {
|
2022-08-30 09:44:56 +00:00
|
|
|
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" {
|
2023-03-09 09:36:55 +00:00
|
|
|
assert.Equal(t, "", req.Header.Get("Authorization"))
|
2022-08-30 09:44:56 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-08-31 18:26:04 +00:00
|
|
|
chksum := "039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81" //nolint:gosec
|
2022-08-30 09:44:56 +00:00
|
|
|
|
|
|
|
t.Run("Invalid Checksum", func(t *testing.T) {
|
|
|
|
spec := PluginSpec{
|
|
|
|
PluginDownloadURL: "",
|
|
|
|
Name: "mockdl",
|
|
|
|
Version: &version,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-08-30 09:44:56 +00:00
|
|
|
Checksums: map[string][]byte{
|
|
|
|
"darwin-amd64": {0},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-08-30 09:44:56 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2022-08-30 09:44:56 +00:00
|
|
|
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,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-08-30 09:44:56 +00:00
|
|
|
Checksums: map[string][]byte{
|
|
|
|
"darwin-amd64": checksum,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-08-30 09:44:56 +00:00
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2023-01-06 22:39:16 +00:00
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-08-25 15:26:25 +00:00
|
|
|
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,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-08-25 15:26:25 +00:00
|
|
|
Checksums: map[string][]byte{
|
|
|
|
"windows-amd64": {0},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
source, err := spec.GetSource()
|
|
|
|
require.NoError(t, err)
|
|
|
|
r, l, err := source.Download(*spec.Version, "darwin", "amd64", getHTTPResponse)
|
|
|
|
require.NoError(t, err)
|
|
|
|
readBytes, err := io.ReadAll(r)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-08-30 09:44:56 +00:00
|
|
|
assert.Equal(t, int(l), len(readBytes))
|
|
|
|
assert.Equal(t, expectedBytes, readBytes)
|
|
|
|
})
|
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
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,
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-02-11 11:05:06 +00:00
|
|
|
}
|
|
|
|
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())
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization"))
|
2023-02-11 11:05:06 +00:00
|
|
|
assert.Equal(t, "application/octet-stream", req.Header.Get("Accept"))
|
|
|
|
return newMockReadCloser(expectedBytes)
|
|
|
|
}
|
|
|
|
r, l, err := source.Download(*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)
|
|
|
|
})
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-04 08:17:41 +00:00
|
|
|
//nolint:paralleltest // mutates environment variables
|
2022-03-03 09:22:21 +00:00
|
|
|
func TestPluginGetLatestVersion(t *testing.T) {
|
2022-03-14 21:14:36 +00:00
|
|
|
token := "RaNd0m70K3n_"
|
|
|
|
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Pulumi GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", "")
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-03-03 09:22:21 +00:00
|
|
|
PluginDownloadURL: "",
|
2022-03-04 08:17:41 +00:00
|
|
|
Name: "mock-latest",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
|
|
|
expectedVersion := semver.MustParse("4.37.5")
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2022-06-29 19:15:01 +00:00
|
|
|
assert.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
|
|
assert.Equal(t,
|
2022-03-04 08:17:41 +00:00
|
|
|
"https://api.github.com/repos/pulumi/pulumi-mock-latest/releases/latest",
|
2022-03-03 09:22:21 +00:00
|
|
|
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(getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
assert.Equal(t, expectedVersion, *version)
|
|
|
|
})
|
2023-02-15 19:48:02 +00:00
|
|
|
t.Run("Custom http URL", func(t *testing.T) {
|
|
|
|
spec := PluginSpec{
|
|
|
|
PluginDownloadURL: "http://customurl.jfrog.io/artifactory/pulumi-packages/package-name",
|
|
|
|
Name: "mock-latest",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-02-15 19:48:02 +00:00
|
|
|
}
|
|
|
|
source, err := spec.GetSource()
|
|
|
|
require.NoError(t, err)
|
|
|
|
version, err := source.GetLatestVersion(getHTTPResponse)
|
|
|
|
assert.Nil(t, version)
|
2023-12-20 15:54:06 +00:00
|
|
|
assert.EqualError(t, err, "GetLatestVersion is not supported for plugins from http sources")
|
2023-02-15 19:48:02 +00:00
|
|
|
})
|
|
|
|
t.Run("Custom https URL", func(t *testing.T) {
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-03-03 09:22:21 +00:00
|
|
|
PluginDownloadURL: "https://customurl.jfrog.io/artifactory/pulumi-packages/package-name",
|
2022-03-04 08:17:41 +00:00
|
|
|
Name: "mock-latest",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-03-03 09:22:21 +00:00
|
|
|
}
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-03 09:22:21 +00:00
|
|
|
version, err := source.GetLatestVersion(getHTTPResponse)
|
|
|
|
assert.Nil(t, version)
|
2023-12-20 15:54:06 +00:00
|
|
|
assert.EqualError(t, err, "GetLatestVersion is not supported for plugins from http sources")
|
2022-02-17 21:20:29 +00:00
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Private Pulumi GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", token)
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-03-14 21:14:36 +00:00
|
|
|
PluginDownloadURL: "",
|
|
|
|
Name: "mock-private",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-03-14 21:14:36 +00:00
|
|
|
}
|
|
|
|
expectedVersion := semver.MustParse("4.37.5")
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
getHTTPResponse := func(req *http.Request) (io.ReadCloser, int64, error) {
|
|
|
|
if req.URL.String() == "https://api.github.com/repos/pulumi/pulumi-mock-private/releases/latest" {
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-03-14 21:14:36 +00:00
|
|
|
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(getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-14 21:14:36 +00:00
|
|
|
assert.Equal(t, expectedVersion, *version)
|
|
|
|
})
|
2023-02-11 11:05:06 +00:00
|
|
|
t.Run("Internal GitHub Releases", func(t *testing.T) {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("GITHUB_TOKEN", token)
|
2022-08-26 14:51:14 +00:00
|
|
|
spec := PluginSpec{
|
2022-06-29 19:15:01 +00:00
|
|
|
PluginDownloadURL: "github://api.git.org/ourorg/mock",
|
|
|
|
Name: "mock-private",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2022-06-29 19:15:01 +00:00
|
|
|
}
|
|
|
|
expectedVersion := semver.MustParse("4.37.5")
|
2022-08-26 14:51:14 +00:00
|
|
|
source, err := spec.GetSource()
|
2022-06-29 19:15:01 +00:00
|
|
|
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" {
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "token "+token, req.Header.Get("Authorization"))
|
2022-06-29 19:15:01 +00:00
|
|
|
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(getHTTPResponse)
|
2023-02-11 11:05:06 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-02-11 11:05:06 +00:00
|
|
|
}
|
|
|
|
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" {
|
2023-12-12 12:19:42 +00:00
|
|
|
assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization"))
|
2023-02-11 11:05:06 +00:00
|
|
|
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(getHTTPResponse)
|
|
|
|
require.NoError(t, err)
|
2022-06-29 19:15:01 +00:00
|
|
|
assert.Equal(t, expectedVersion, *version)
|
|
|
|
})
|
2023-03-02 13:34:36 +00:00
|
|
|
t.Run("Hit GitHub ratelimit", func(t *testing.T) {
|
|
|
|
t.Setenv("GITHUB_TOKEN", "")
|
|
|
|
spec := PluginSpec{
|
|
|
|
PluginDownloadURL: "",
|
|
|
|
Name: "mock-latest",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.PluginKind("resource"),
|
2023-03-02 13:34:36 +00:00
|
|
|
}
|
|
|
|
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(getHTTPResponse)
|
2023-12-08 06:40:14 +00:00
|
|
|
assert.ErrorContains(t, err, "rate limit exceeded")
|
|
|
|
assert.ErrorContains(t, err, "https://api.github.com/repos/pulumi/pulumi-mock-latest/releases/latest")
|
2023-03-02 13:34:36 +00:00
|
|
|
})
|
2022-01-20 15:50:11 +00:00
|
|
|
}
|
|
|
|
|
2022-01-21 17:04:10 +00:00
|
|
|
func TestParsePluginDownloadURLOverride(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2022-01-21 17:04:10 +00:00
|
|
|
type match struct {
|
|
|
|
name string
|
|
|
|
url string
|
|
|
|
ok bool
|
|
|
|
}
|
|
|
|
|
|
|
|
tests := []struct {
|
2023-12-08 06:40:14 +00:00
|
|
|
input string
|
|
|
|
expected pluginDownloadOverrideArray
|
|
|
|
matches []match
|
|
|
|
expectedError string
|
2022-01-21 17:04:10 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
input: "",
|
2023-01-11 22:38:33 +00:00
|
|
|
expected: pluginDownloadOverrideArray{},
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2023-12-08 06:40:14 +00:00
|
|
|
input: "=", // missing regex and url
|
|
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"=\"",
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
{
|
2023-12-08 06:40:14 +00:00
|
|
|
input: "^foo.*=", // missing url
|
|
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"^foo.*=\"",
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
{
|
2023-12-08 06:40:14 +00:00
|
|
|
input: "=https://foo", // missing regex
|
|
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"=https://foo\"",
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
{
|
2023-12-08 06:40:14 +00:00
|
|
|
input: "^foo.*=https://foo,", // trailing comma
|
|
|
|
expectedError: "expected format to be \"regexp1=URL1,regexp2=URL2\"; got \"^foo.*=https://foo,\"",
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
{
|
2023-12-08 06:40:14 +00:00
|
|
|
input: "[=https://foo", // invalid regex
|
|
|
|
expectedError: "error parsing regexp: missing closing ]: `[`",
|
2022-01-21 17:04:10 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
2022-03-04 08:17:41 +00:00
|
|
|
tt := tt
|
2022-01-21 17:04:10 +00:00
|
|
|
t.Run(tt.input, func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2022-01-21 17:04:10 +00:00
|
|
|
actual, err := parsePluginDownloadURLOverrides(tt.input)
|
2023-12-08 06:40:14 +00:00
|
|
|
if tt.expectedError != "" {
|
|
|
|
assert.EqualError(t, err, tt.expectedError)
|
2022-01-21 17:04:10 +00:00
|
|
|
} 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-03-17 12:22:56 +00:00
|
|
|
|
URL-based plugin source overrides via env var (#16648)
### Motivation
Pulumi plugin binaries can be downloaded by the CLI from multiple
sources. By default, it's downloaded from Pulumi's GitHub releases or
get.pulumi.com, but plugins can also specify their binary sources via
the `PluginDownloadURL` schema option. They can point to custom GitHub,
Gitlab, or HTTP locations.
Enterprise customers ask for a way to isolate the CLI from downloads
from random locations and to configure the CLI to go to their internal
pre-approved artefact location instead. This way, Pulumi can run in
"air-gapped" environments (which still have access to Cloud APIs, of
course).
Related issues:
- https://github.com/pulumi/pulumi/issues/14459
- https://github.com/pulumi/pulumi/issues/16240
Currently, there is a basic mechanism to do so via the variable
`pluginDownloadURLOverrides`, but it has two major limitations:
- The variable value is set via a compile-time flag, so it requires a
custom build of the CLI
- The overrides are based on the plugin name, so the rules must be
defined without access to the original URL, which makes it hard to
provide universal rules and still distinguish between first-party,
public third-party, or private in-house plugins
- We ignore overrides for all plugins that have `PluginDownloadURL` set
- Overrides can set a plugin replacement redirect only to HTTP(s)
addresses
### Proposal
This PR makes two sets of changes:
1. It allows passing overrides via the
`PULUMI_PLUGIN_DOWNLOAD_URL_OVERRIDES` environment variable. The
compile-time flag is still supported, but the env var takes priority.
More configuration levers could be supported, but it not clear if we
have good ones until [Support .pulumirc file for global
config](https://github.com/pulumi/pulumi/issues/13484) is implemented. I
don't expect users to want to set this via their stack configs, but I'm
curious what others think. In any case, more sources can be added later.
2. The overrides now apply based on the original download URL, not just
on plugin names. Actually, it's the base URL of a download source that
is passed to the regexp matcher. Examples of possible options are:
- `github://api.github.com/pulumi/pulumi-xyz` for a first-party plugin
(note that we don't pass `get.pulumi.com`
- `github://api.github.com/pulumiverse/pulumi-grafana` for a community
plugin that sets `PluginDownloadURL`
- `gitlab://gitlab-host/proj-name` for a community plugin hosted on
Gitlab
- `https://example.com/downloads/` for HTTP sources
So, the override
`^github://api.github.com/pulumi/pulumi-xyz=https://example.com/downloads/pulumi-xyz/`
will override the single provider URL from our GitHub releases to the
given HTTP location.
On top of that, regular expressions may contain name groups to capture
and use templated values. For example,
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/${org}/${repo}`
captures any GitHub plugin and redirects it to its corresponding HTTP
location. Group indices are also supported: the above override can also
be written as
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/$1/$2`,
with `$0` meaning the full match.
The override URLs have the same semantics as `PluginDownloadURL`, so
they can point to GitHub, Gitlab, HTTP, or anything we introduce in the
future.
### Impact
Technically, this is a breaking change, because name-based overrides
will stop working. However, we are fairly certain that we have a single
customer using the existing compile-time approach, and they indicated
that they don't need the name-based overrides if they have URL-based
overrides. I reviewed this PR with them and made sure they can migrate
immediately after the change is released.
Backwards compatibility is slightly tricky, because we'd need to keep
name-based override _and_ not applying them to third-party plugins. But
we can do it if necessary.
Resolve #16240
2024-07-26 10:37:09 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-20 19:58:59 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.LanguagePlugin,
|
2023-03-20 19:58:59 +00:00
|
|
|
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
|
2023-03-20 20:20:38 +00:00
|
|
|
currentTime := time.Now()
|
2023-03-20 20:13:33 +00:00
|
|
|
_, err := (&pluginDownloader{
|
|
|
|
OnRetry: func(err error, attempt, limit int, delay time.Duration) {
|
2023-03-20 19:58:59 +00:00
|
|
|
assert.Equal(t, 5, limit, "unexpected retry limit")
|
|
|
|
numRetries++
|
|
|
|
assert.Equal(t, numRetries, attempt, "unexpected attempt number")
|
|
|
|
},
|
2023-03-20 20:20:38 +00:00
|
|
|
After: func(d time.Duration) <-chan time.Time {
|
|
|
|
currentTime = currentTime.Add(d)
|
|
|
|
ch := make(chan time.Time, 1)
|
|
|
|
ch <- currentTime
|
|
|
|
return ch
|
|
|
|
},
|
2023-03-20 20:13:33 +00:00
|
|
|
}).DownloadToFile(spec)
|
2023-03-20 19:58:59 +00:00
|
|
|
assert.ErrorContains(t, err, "failed to download plugin: myplugin-1.0.0")
|
|
|
|
assert.Equal(t, numRequests, numRetries)
|
|
|
|
}
|
|
|
|
|
2022-07-22 13:17:43 +00:00
|
|
|
//nolint:paralleltest // changes directory for process
|
|
|
|
func TestUnmarshalProjectWithProviderList(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-12-03 07:17:08 +00:00
|
|
|
tempdir := t.TempDir()
|
2022-07-22 13:17:43 +00:00
|
|
|
pyaml := filepath.Join(tempdir, "Pulumi.yaml")
|
|
|
|
|
2023-03-03 16:36:39 +00:00
|
|
|
// write to pyaml
|
2023-01-06 22:39:16 +00:00
|
|
|
err := os.WriteFile(pyaml, []byte(`name: test-yaml
|
2022-07-22 13:17:43 +00:00
|
|
|
runtime: yaml
|
|
|
|
description: "Test Pulumi YAML"
|
|
|
|
plugins:
|
|
|
|
providers:
|
|
|
|
- name: aws
|
|
|
|
version: 1.0.0
|
2023-03-03 16:36:39 +00:00
|
|
|
path: ../bin/aws`), 0o600)
|
2022-07-22 13:17:43 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-02-15 19:48:02 +00:00
|
|
|
|
URL-based plugin source overrides via env var (#16648)
### Motivation
Pulumi plugin binaries can be downloaded by the CLI from multiple
sources. By default, it's downloaded from Pulumi's GitHub releases or
get.pulumi.com, but plugins can also specify their binary sources via
the `PluginDownloadURL` schema option. They can point to custom GitHub,
Gitlab, or HTTP locations.
Enterprise customers ask for a way to isolate the CLI from downloads
from random locations and to configure the CLI to go to their internal
pre-approved artefact location instead. This way, Pulumi can run in
"air-gapped" environments (which still have access to Cloud APIs, of
course).
Related issues:
- https://github.com/pulumi/pulumi/issues/14459
- https://github.com/pulumi/pulumi/issues/16240
Currently, there is a basic mechanism to do so via the variable
`pluginDownloadURLOverrides`, but it has two major limitations:
- The variable value is set via a compile-time flag, so it requires a
custom build of the CLI
- The overrides are based on the plugin name, so the rules must be
defined without access to the original URL, which makes it hard to
provide universal rules and still distinguish between first-party,
public third-party, or private in-house plugins
- We ignore overrides for all plugins that have `PluginDownloadURL` set
- Overrides can set a plugin replacement redirect only to HTTP(s)
addresses
### Proposal
This PR makes two sets of changes:
1. It allows passing overrides via the
`PULUMI_PLUGIN_DOWNLOAD_URL_OVERRIDES` environment variable. The
compile-time flag is still supported, but the env var takes priority.
More configuration levers could be supported, but it not clear if we
have good ones until [Support .pulumirc file for global
config](https://github.com/pulumi/pulumi/issues/13484) is implemented. I
don't expect users to want to set this via their stack configs, but I'm
curious what others think. In any case, more sources can be added later.
2. The overrides now apply based on the original download URL, not just
on plugin names. Actually, it's the base URL of a download source that
is passed to the regexp matcher. Examples of possible options are:
- `github://api.github.com/pulumi/pulumi-xyz` for a first-party plugin
(note that we don't pass `get.pulumi.com`
- `github://api.github.com/pulumiverse/pulumi-grafana` for a community
plugin that sets `PluginDownloadURL`
- `gitlab://gitlab-host/proj-name` for a community plugin hosted on
Gitlab
- `https://example.com/downloads/` for HTTP sources
So, the override
`^github://api.github.com/pulumi/pulumi-xyz=https://example.com/downloads/pulumi-xyz/`
will override the single provider URL from our GitHub releases to the
given HTTP location.
On top of that, regular expressions may contain name groups to capture
and use templated values. For example,
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/${org}/${repo}`
captures any GitHub plugin and redirects it to its corresponding HTTP
location. Group indices are also supported: the above override can also
be written as
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/$1/$2`,
with `$0` meaning the full match.
The override URLs have the same semantics as `PluginDownloadURL`, so
they can point to GitHub, Gitlab, HTTP, or anything we introduce in the
future.
### Impact
Technically, this is a breaking change, because name-based overrides
will stop working. However, we are fairly certain that we have a single
customer using the existing compile-time approach, and they indicated
that they don't need the name-based overrides if they have URL-based
overrides. I reviewed this PR with them and made sure they can migrate
immediately after the change is released.
Backwards compatibility is slightly tricky, because we'd need to keep
name-based override _and_ not applying them to third-party plugins. But
we can do it if necessary.
Resolve #16240
2024-07-26 10:37:09 +00:00
|
|
|
//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",
|
|
|
|
},
|
|
|
|
}
|
2023-02-15 19:48:02 +00:00
|
|
|
|
URL-based plugin source overrides via env var (#16648)
### Motivation
Pulumi plugin binaries can be downloaded by the CLI from multiple
sources. By default, it's downloaded from Pulumi's GitHub releases or
get.pulumi.com, but plugins can also specify their binary sources via
the `PluginDownloadURL` schema option. They can point to custom GitHub,
Gitlab, or HTTP locations.
Enterprise customers ask for a way to isolate the CLI from downloads
from random locations and to configure the CLI to go to their internal
pre-approved artefact location instead. This way, Pulumi can run in
"air-gapped" environments (which still have access to Cloud APIs, of
course).
Related issues:
- https://github.com/pulumi/pulumi/issues/14459
- https://github.com/pulumi/pulumi/issues/16240
Currently, there is a basic mechanism to do so via the variable
`pluginDownloadURLOverrides`, but it has two major limitations:
- The variable value is set via a compile-time flag, so it requires a
custom build of the CLI
- The overrides are based on the plugin name, so the rules must be
defined without access to the original URL, which makes it hard to
provide universal rules and still distinguish between first-party,
public third-party, or private in-house plugins
- We ignore overrides for all plugins that have `PluginDownloadURL` set
- Overrides can set a plugin replacement redirect only to HTTP(s)
addresses
### Proposal
This PR makes two sets of changes:
1. It allows passing overrides via the
`PULUMI_PLUGIN_DOWNLOAD_URL_OVERRIDES` environment variable. The
compile-time flag is still supported, but the env var takes priority.
More configuration levers could be supported, but it not clear if we
have good ones until [Support .pulumirc file for global
config](https://github.com/pulumi/pulumi/issues/13484) is implemented. I
don't expect users to want to set this via their stack configs, but I'm
curious what others think. In any case, more sources can be added later.
2. The overrides now apply based on the original download URL, not just
on plugin names. Actually, it's the base URL of a download source that
is passed to the regexp matcher. Examples of possible options are:
- `github://api.github.com/pulumi/pulumi-xyz` for a first-party plugin
(note that we don't pass `get.pulumi.com`
- `github://api.github.com/pulumiverse/pulumi-grafana` for a community
plugin that sets `PluginDownloadURL`
- `gitlab://gitlab-host/proj-name` for a community plugin hosted on
Gitlab
- `https://example.com/downloads/` for HTTP sources
So, the override
`^github://api.github.com/pulumi/pulumi-xyz=https://example.com/downloads/pulumi-xyz/`
will override the single provider URL from our GitHub releases to the
given HTTP location.
On top of that, regular expressions may contain name groups to capture
and use templated values. For example,
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/${org}/${repo}`
captures any GitHub plugin and redirects it to its corresponding HTTP
location. Group indices are also supported: the above override can also
be written as
`^github://api.github.com/(?P<org>[^/]+)/(?P<repo>[^/]+)=https://example.com/downloads/$1/$2`,
with `$0` meaning the full match.
The override URLs have the same semantics as `PluginDownloadURL`, so
they can point to GitHub, Gitlab, HTTP, or anything we introduce in the
future.
### Impact
Technically, this is a breaking change, because name-based overrides
will stop working. However, we are fairly certain that we have a single
customer using the existing compile-time approach, and they indicated
that they don't need the name-based overrides if they have URL-based
overrides. I reviewed this PR with them and made sure they can migrate
immediately after the change is released.
Backwards compatibility is slightly tricky, because we'd need to keep
name-based override _and_ not applying them to third-party plugins. But
we can do it if necessary.
Resolve #16240
2024-07-26 10:37:09 +00:00
|
|
|
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())
|
|
|
|
})
|
2023-02-15 19:48:02 +00:00
|
|
|
}
|
|
|
|
}
|
2023-03-15 15:24:39 +00:00
|
|
|
|
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-03-15 15:24:39 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-03-15 15:24:39 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-03-15 15:24:39 +00:00
|
|
|
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",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.ResourcePlugin,
|
2023-03-15 15:24:39 +00:00
|
|
|
Version: nil,
|
|
|
|
},
|
|
|
|
IncludeAmbient: false,
|
|
|
|
ExpectedError: "no resource plugin 'pulumi-resource-myplugin' found in the workspace",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "LanguageWithoutVersion",
|
|
|
|
Plugin: PluginInfo{
|
|
|
|
Name: "dotnet",
|
2024-04-25 17:30:30 +00:00
|
|
|
Kind: apitype.LanguagePlugin,
|
2023-03-15 15:24:39 +00:00
|
|
|
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)
|
2023-12-20 15:54:06 +00:00
|
|
|
assert.EqualError(t, err, tt.ExpectedError)
|
2023-03-15 15:24:39 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-07-18 14:21:44 +00:00
|
|
|
|
|
|
|
//nolint:paralleltest // modifies environment variables
|
|
|
|
func TestBundledPluginSearch(t *testing.T) {
|
|
|
|
// Get the path of this executable
|
|
|
|
exe, err := os.Executable()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-03-31 18:33:55 +00:00
|
|
|
// 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")
|
2023-07-18 14:21:44 +00:00
|
|
|
err = os.WriteFile(bundledPath, []byte{}, 0o700) //nolint: gosec // we intended to write an executable file here
|
|
|
|
require.NoError(t, err)
|
2023-10-09 18:31:17 +00:00
|
|
|
bundledPath, _ = filepath.EvalSymlinks(bundledPath)
|
2023-07-18 14:21:44 +00:00
|
|
|
t.Cleanup(func() {
|
|
|
|
err := os.Remove(bundledPath)
|
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
|
|
|
|
2023-03-31 18:33:55 +00:00
|
|
|
// Create another copy of the fake plugin in $PATH
|
2023-07-18 14:21:44 +00:00
|
|
|
pathDir := t.TempDir()
|
|
|
|
t.Setenv("PATH", pathDir)
|
2023-03-31 18:33:55 +00:00
|
|
|
ambientPath := filepath.Join(pathDir, "pulumi-language-nodejs")
|
2023-07-18 14:21:44 +00:00
|
|
|
err = os.WriteFile(ambientPath, []byte{}, 0o700) //nolint: gosec
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-08-07 12:15:57 +00:00
|
|
|
d := diagtest.LogSink(t)
|
|
|
|
|
2023-07-18 14:21:44 +00:00
|
|
|
// Lookup the plugin with ambient search turned on
|
|
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "false")
|
2024-04-25 17:30:30 +00:00
|
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
2023-07-18 14:21:44 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, ambientPath, path)
|
|
|
|
|
|
|
|
// Lookup the plugin with ambient search turned off
|
|
|
|
t.Setenv("PULUMI_IGNORE_AMBIENT_PLUGINS", "true")
|
2024-04-25 17:30:30 +00:00
|
|
|
path, err = GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
2023-07-18 14:21:44 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, bundledPath, path)
|
|
|
|
}
|
2023-08-07 12:15:57 +00:00
|
|
|
|
|
|
|
//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")
|
2024-04-25 17:30:30 +00:00
|
|
|
path, err := GetPluginPath(d, apitype.ResourcePlugin, "mock", nil, nil)
|
2023-08-07 12:15:57 +00:00
|
|
|
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")
|
2024-04-25 17:30:30 +00:00
|
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
2023-08-07 12:15:57 +00:00
|
|
|
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")
|
2024-04-25 17:30:30 +00:00
|
|
|
path, err := GetPluginPath(d, apitype.LanguagePlugin, "nodejs", nil, nil)
|
2023-08-07 12:15:57 +00:00
|
|
|
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())
|
|
|
|
}
|
2024-08-30 15:51:15 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2024-09-04 10:08:44 +00:00
|
|
|
|
|
|
|
//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)
|
|
|
|
}
|