stacks: add the packages service to the rpc api (#34319)

* stacks: add the packages service to the rpc api

* go generate
pull/34331/head
Liam Cervante 2 years ago committed by GitHub
parent cbfbe51556
commit e703eb7b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,75 @@
package dynrpcserver
import (
"context"
"sync"
tf1 "github.com/hashicorp/terraform/internal/rpcapi/terraform1"
)
type Packages struct {
impl tf1.PackagesServer
mu sync.RWMutex
}
var _ tf1.PackagesServer = (*Packages)(nil)
func NewPackagesStub() *Packages {
return &Packages{}
}
func (s *Packages) FetchModulePackage(a0 context.Context, a1 *tf1.FetchModulePackage_Request) (*tf1.FetchModulePackage_Response, error) {
impl, err := s.realRPCServer()
if err != nil {
return nil, err
}
return impl.FetchModulePackage(a0, a1)
}
func (s *Packages) FetchProviderPackage(a0 context.Context, a1 *tf1.FetchProviderPackage_Request) (*tf1.FetchProviderPackage_Response, error) {
impl, err := s.realRPCServer()
if err != nil {
return nil, err
}
return impl.FetchProviderPackage(a0, a1)
}
func (s *Packages) ModulePackageSourceAddr(a0 context.Context, a1 *tf1.ModulePackageSourceAddr_Request) (*tf1.ModulePackageSourceAddr_Response, error) {
impl, err := s.realRPCServer()
if err != nil {
return nil, err
}
return impl.ModulePackageSourceAddr(a0, a1)
}
func (s *Packages) ModulePackageVersions(a0 context.Context, a1 *tf1.ModulePackageVersions_Request) (*tf1.ModulePackageVersions_Response, error) {
impl, err := s.realRPCServer()
if err != nil {
return nil, err
}
return impl.ModulePackageVersions(a0, a1)
}
func (s *Packages) ProviderPackageVersions(a0 context.Context, a1 *tf1.ProviderPackageVersions_Request) (*tf1.ProviderPackageVersions_Response, error) {
impl, err := s.realRPCServer()
if err != nil {
return nil, err
}
return impl.ProviderPackageVersions(a0, a1)
}
func (s *Packages) ActivateRPCServer(impl tf1.PackagesServer) {
s.mu.Lock()
s.impl = impl
s.mu.Unlock()
}
func (s *Packages) realRPCServer() (tf1.PackagesServer, error) {
s.mu.RLock()
impl := s.impl
s.mu.RUnlock()
if impl == nil {
return nil, unavailableErr
}
return impl, nil
}

@ -0,0 +1,256 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package rpcapi
import (
"context"
"fmt"
"strings"
"github.com/apparentlymart/go-versions/versions"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/getmodules"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/registry"
"github.com/hashicorp/terraform/internal/registry/regsrc"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
)
var _ terraform1.PackagesServer = (*packagesServer)(nil)
func newPackagesServer(services *disco.Disco) *packagesServer {
return &packagesServer{
services: services,
// This function lets us control the provider source during tests.
providerSourceFn: func(services *disco.Disco) getproviders.Source {
// TODO: Implement loading from alternate sources like network or filesystem
// mirrors.
return getproviders.NewRegistrySource(services)
},
}
}
type providerSourceFn func(services *disco.Disco) getproviders.Source
type packagesServer struct {
terraform1.UnimplementedPackagesServer
services *disco.Disco
providerSourceFn providerSourceFn
}
func (p *packagesServer) ProviderPackageVersions(ctx context.Context, request *terraform1.ProviderPackageVersions_Request) (*terraform1.ProviderPackageVersions_Response, error) {
response := new(terraform1.ProviderPackageVersions_Response)
source := p.providerSourceFn(p.services)
provider, diags := addrs.ParseProviderSourceString(request.SourceAddr)
response.Diagnostics = append(response.Diagnostics, diagnosticsToProto(diags)...)
if diags.HasErrors() {
return response, nil
}
versions, warnings, err := source.AvailableVersions(ctx, provider)
displayWarnings := make([]string, len(warnings))
for ix, warning := range warnings {
displayWarnings[ix] = fmt.Sprintf("- %s", warning)
}
if len(displayWarnings) > 0 {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_WARNING,
Summary: "Additional provider information from registry",
Detail: fmt.Sprintf("The remote registry returned warnings for %s:\n%s", provider.ForDisplay(), strings.Join(displayWarnings, "\n")),
})
}
if err != nil {
// TODO: Parse the different error types so we can provide specific
// error diagnostics, see commands/init.go:621.
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to query available provider packages",
Detail: fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s.", provider.ForDisplay(), err),
})
return response, nil
}
for _, version := range versions {
response.Versions = append(response.Versions, version.String())
}
return response, nil
}
func (p *packagesServer) FetchProviderPackage(ctx context.Context, request *terraform1.FetchProviderPackage_Request) (*terraform1.FetchProviderPackage_Response, error) {
response := new(terraform1.FetchProviderPackage_Response)
version, err := versions.ParseVersion(request.Version)
if err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Invalid platform",
Detail: fmt.Sprintf("The requested version %s is invalid: %s.", request.Version, err),
})
return response, nil
}
source := p.providerSourceFn(p.services)
provider, diags := addrs.ParseProviderSourceString(request.SourceAddr)
response.Diagnostics = append(response.Diagnostics, diagnosticsToProto(diags)...)
if diags.HasErrors() {
return response, nil
}
var allowedHashes []getproviders.Hash
for _, hash := range request.Hashes {
allowedHashes = append(allowedHashes, getproviders.Hash(hash))
}
for _, requestPlatform := range request.Platforms {
result := new(terraform1.FetchProviderPackage_PlatformResult)
response.Results = append(response.Results, result)
platform, err := getproviders.ParsePlatform(requestPlatform)
if err != nil {
result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Invalid platform",
Detail: fmt.Sprintf("The requested platform %s is invalid: %s.", requestPlatform, err),
})
continue
}
meta, err := source.PackageMeta(ctx, provider, version, platform)
if err != nil {
// TODO: Parse the different error types so we can provide specific
// error diagnostics, see commands/init.go:731.
result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to query provider package metadata",
Detail: fmt.Sprintf("Could not retrieve package metadata for provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err),
})
continue
}
into := providercache.NewDirWithPlatform(request.CacheDir, platform)
authResult, err := into.InstallPackage(ctx, meta, allowedHashes)
if err != nil {
// TODO: Parse the different error types so we can provide specific
// error diagnostics, see commands/init.go:731.
result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to download provider package",
Detail: fmt.Sprintf("Could not download provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err),
})
continue
}
var hashes []string
if authResult.SignedByAnyParty() {
for _, hash := range meta.AcceptableHashes() {
hashes = append(hashes, string(hash))
}
}
providerPackage := into.ProviderVersion(provider, version)
hash, err := providerPackage.Hash()
if err != nil {
result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to hash provider package",
Detail: fmt.Sprintf("Could not hash provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err),
})
continue
}
hashes = append(hashes, string(hash))
result.Provider = &terraform1.ProviderPackage{
SourceAddr: request.SourceAddr,
Version: request.Version,
Hashes: hashes,
}
}
return response, nil
}
func (p *packagesServer) ModulePackageVersions(ctx context.Context, request *terraform1.ModulePackageVersions_Request) (*terraform1.ModulePackageVersions_Response, error) {
response := new(terraform1.ModulePackageVersions_Response)
module, err := regsrc.ParseModuleSource(request.SourceAddr)
if err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Invalid module source",
Detail: fmt.Sprintf("Module source %s is invalid: %s.", request.SourceAddr, err),
})
return response, nil
}
client := registry.NewClient(p.services, nil)
versions, err := client.ModuleVersions(ctx, module)
if err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to query available module packages",
Detail: fmt.Sprintf("Could not retrieve the list of available modules for module %s: %s.", module.Display(), err),
})
return response, nil
}
for _, module := range versions.Modules {
for _, version := range module.Versions {
response.Versions = append(response.Versions, version.Version)
}
}
return response, nil
}
func (p *packagesServer) ModulePackageSourceAddr(ctx context.Context, request *terraform1.ModulePackageSourceAddr_Request) (*terraform1.ModulePackageSourceAddr_Response, error) {
response := new(terraform1.ModulePackageSourceAddr_Response)
module, err := regsrc.ParseModuleSource(request.SourceAddr)
if err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Invalid module source",
Detail: fmt.Sprintf("Module source %s is invalid: %s.", request.SourceAddr, err),
})
return response, nil
}
client := registry.NewClient(p.services, nil)
location, err := client.ModuleLocation(ctx, module, request.Version)
if err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to query module package metadata",
Detail: fmt.Sprintf("Could not retrieve package metadata for provider %s at %s: %s.", module.Display(), request.Version, err),
})
return response, nil
}
response.Url = location
return response, nil
}
func (p *packagesServer) FetchModulePackage(ctx context.Context, request *terraform1.FetchModulePackage_Request) (*terraform1.FetchModulePackage_Response, error) {
response := new(terraform1.FetchModulePackage_Response)
fetcher := getmodules.NewPackageFetcher()
if err := fetcher.FetchPackage(ctx, request.CacheDir, request.Url); err != nil {
response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{
Severity: terraform1.Diagnostic_ERROR,
Summary: "Failed to download module package",
Detail: fmt.Sprintf("Could not download provider from %s: %s.", request.Url, err),
})
return response, nil
}
return response, nil
}

@ -0,0 +1,307 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package rpcapi
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"strings"
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
)
func TestPackagesServer_ProviderPackageVersions(t *testing.T) {
tcs := map[string]struct {
source string
expectedVersions []string
expectedWarnings []string
sourceFn providerSourceFn
}{
"single_version": {
source: "hashicorp/foo",
expectedVersions: []string{"0.1.0"},
sourceFn: func(_ *disco.Disco) getproviders.Source {
packages := []getproviders.PackageMeta{
{
Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
Version: versions.MustParseVersion("0.1.0"),
},
}
return getproviders.NewMockSource(packages, nil)
},
},
"multiple_versions": {
source: "hashicorp/foo",
expectedVersions: []string{"0.1.0", "0.2.0"},
sourceFn: func(_ *disco.Disco) getproviders.Source {
packages := []getproviders.PackageMeta{
{
Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
Version: versions.MustParseVersion("0.1.0"),
},
{
Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
Version: versions.MustParseVersion("0.2.0"),
},
}
return getproviders.NewMockSource(packages, nil)
},
},
"with_warnings": {
source: "hashicorp/foo",
expectedVersions: []string{"0.1.0"},
expectedWarnings: []string{"- warning one", "- warning two"},
sourceFn: func(_ *disco.Disco) getproviders.Source {
packages := []getproviders.PackageMeta{
{
Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
Version: versions.MustParseVersion("0.1.0"),
},
}
warnings := map[addrs.Provider]getproviders.Warnings{
addrs.MustParseProviderSourceString("hashicorp/foo"): {
"warning one",
"warning two",
},
}
return getproviders.NewMockSource(packages, warnings)
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
service := &packagesServer{
providerSourceFn: tc.sourceFn,
}
response, err := service.ProviderPackageVersions(context.Background(), &terraform1.ProviderPackageVersions_Request{
SourceAddr: tc.source,
})
if err != nil {
t.Fatal(err)
}
if len(tc.expectedWarnings) > 0 {
for _, diag := range response.Diagnostics {
if diag.Severity == terraform1.Diagnostic_WARNING && diag.Summary == "Additional provider information from registry" {
expected := fmt.Sprintf("The remote registry returned warnings for %s:\n%s", tc.source, strings.Join(tc.expectedWarnings, "\n"))
if diff := cmp.Diff(expected, diag.Detail); len(diff) > 0 {
t.Errorf(diff)
}
}
}
// We're expecting only one diagnostic with the warnings.
if len(response.Diagnostics) > 1 {
for _, diag := range response.Diagnostics {
t.Errorf("unexpected diagnostics: %s", diag.Detail)
}
return
}
} else {
// Otherwise we're expecting no diagnostics.
if len(response.Diagnostics) > 0 {
for _, diag := range response.Diagnostics {
t.Errorf("unexpected diagnostics: %s", diag.Detail)
}
return
}
}
if diff := cmp.Diff(tc.expectedVersions, response.Versions); len(diff) > 0 {
t.Errorf(diff)
}
})
}
}
func TestPackagesServer_FetchProviderPackage(t *testing.T) {
providerHashes := providerHashes(t)
tcs := map[string]struct {
// source, version, platforms, and hashes are what we're going to pass
// in as the request.
source string
version string
platforms []string
hashes []string
// platformLocations, and platformHashes are what we're going to use to
// create our virtual provider metadata.
platformLocations map[string]string
platformHashes map[string][]string
// diagnostics are the expected diagnostics for each platform.
diagnostics map[string][]string
}{
"single_version_and_platform": {
source: "hashicorp/foo",
version: "0.1.0",
platforms: []string{"linux_amd64"},
platformLocations: map[string]string{
"linux_amd64": "terraform_provider_foo",
},
},
"single_version_multiple_platforms": {
source: "hashicorp/foo",
version: "0.1.0",
platforms: []string{"linux_amd64", "darwin_arm64"},
platformLocations: map[string]string{
"linux_amd64": "terraform_provider_foo",
"darwin_arm64": "terraform_provider_bar",
},
},
"single_version_and_platform_with_hashes": {
source: "hashicorp/foo",
version: "0.1.0",
platforms: []string{"linux_amd64"},
platformLocations: map[string]string{
"linux_amd64": "terraform_provider_foo",
},
platformHashes: map[string][]string{
"linux_amd64": {
"h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=",
},
},
},
"single_version_and_platform_with_hashes_clash": {
source: "hashicorp/foo",
version: "0.1.0",
hashes: []string{"h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="},
platforms: []string{"linux_amd64"},
platformLocations: map[string]string{
"linux_amd64": "terraform_provider_foo",
},
platformHashes: map[string][]string{
"linux_amd64": {
"h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=",
},
},
diagnostics: map[string][]string{
"linux_amd64": {
"the local package for registry.terraform.io/hashicorp/foo 0.1.0 doesn't match any of the checksums previously recorded in the dependency lock file",
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
service := &packagesServer{
providerSourceFn: func(_ *disco.Disco) getproviders.Source {
var providers []getproviders.PackageMeta
for _, p := range tc.platforms {
platform := parsePlatform(t, p)
var authentication getproviders.PackageAuthentication
if len(tc.platformHashes) > 0 {
authentication = getproviders.NewPackageHashAuthentication(platform, func() []getproviders.Hash {
var hashes []getproviders.Hash
for _, hash := range tc.platformHashes[p] {
hashes = append(hashes, getproviders.Hash(hash))
}
return hashes
}())
}
providers = append(providers, getproviders.PackageMeta{
Provider: addrs.MustParseProviderSourceString(tc.source),
Version: versions.MustParseVersion(tc.version),
TargetPlatform: platform,
Location: getproviders.PackageLocalDir(path.Join("testdata", "providers", tc.platformLocations[p])),
Authentication: authentication,
})
}
return getproviders.NewMockSource(providers, nil)
},
}
cacheDir := t.TempDir()
response, err := service.FetchProviderPackage(context.Background(), &terraform1.FetchProviderPackage_Request{
CacheDir: cacheDir,
SourceAddr: tc.source,
Version: tc.version,
Platforms: tc.platforms,
Hashes: tc.hashes,
})
if err != nil {
t.Fatal(err)
}
if len(response.Diagnostics) > 0 {
for _, diag := range response.Diagnostics {
t.Errorf("unexpected diagnostics: %s", diag.Detail)
}
return
}
if len(response.Results) != len(tc.platforms) {
t.Fatalf("wrong number of results")
}
for ix, platform := range tc.platforms {
result := response.Results[ix]
if tc.diagnostics != nil && len(tc.diagnostics[platform]) > 0 {
if len(result.Diagnostics) != len(tc.diagnostics[platform]) {
t.Fatalf("expected %d diagnostics for %s but found %d", len(tc.diagnostics[platform]), platform, len(result.Diagnostics))
}
for ix, expected := range tc.diagnostics[platform] {
if !strings.Contains(result.Diagnostics[ix].Detail, expected) {
t.Errorf("expected: %s\nactual: %s", expected, result.Diagnostics[ix])
}
}
return
} else {
if len(result.Diagnostics) > 0 {
for _, diag := range result.Diagnostics {
t.Errorf("unexpected diagnostics for %s: %s", platform, diag.Detail)
}
return
}
}
if diff := cmp.Diff(providerHashes[tc.platformLocations[platform]], result.Provider.Hashes); len(diff) > 0 {
t.Errorf(diff)
}
}
})
}
}
func providerHashes(t *testing.T) map[string][]string {
var hashes map[string][]string
data, err := os.ReadFile("testdata/providers/hashes.json")
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(data, &hashes); err != nil {
t.Fatal(err)
}
return hashes
}
func parsePlatform(t *testing.T, raw string) getproviders.Platform {
platform, err := getproviders.ParsePlatform(raw)
if err != nil {
t.Fatal(err)
}
return platform
}

@ -9,9 +9,10 @@ import (
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco"
"google.golang.org/grpc"
"github.com/hashicorp/terraform/internal/rpcapi/dynrpcserver"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
"google.golang.org/grpc"
)
type corePlugin struct {
@ -44,6 +45,8 @@ func serverHandshake(s *grpc.Server) func(context.Context, *terraform1.ClientCap
terraform1.RegisterDependenciesServer(s, dependencies)
stacks := dynrpcserver.NewStacksStub()
terraform1.RegisterStacksServer(s, stacks)
packages := dynrpcserver.NewPackagesStub()
terraform1.RegisterPackagesServer(s, packages)
return func(ctx context.Context, clientCaps *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) {
// All of our servers will share a common handles table so that objects
@ -69,6 +72,7 @@ func serverHandshake(s *grpc.Server) func(context.Context, *terraform1.ClientCap
// might vary based on the negotiated capabilities.
dependencies.ActivateRPCServer(newDependenciesServer(handles, services))
stacks.ActivateRPCServer(newStacksServer(handles))
packages.ActivateRPCServer(newPackagesServer(services))
// If the client requested any extra capabililties that we're going
// to honor then we should announce them in this result.

File diff suppressed because it is too large Load Diff

@ -1106,3 +1106,87 @@ message Schema {
}
}
}
// The Packages service provides helper functions for retrieving Terraform
// modules and providers.
//
// Unlike the Dependencies service, the Packages service does not require any
// existing configuration or sourcebundle to function.
//
// This service is designed for use with a specific command-line tool, and is
// currently experimental. It can be changed and removed without warning, even
// in patch releases.
service Packages {
rpc ProviderPackageVersions(ProviderPackageVersions.Request) returns (ProviderPackageVersions.Response);
rpc FetchProviderPackage(FetchProviderPackage.Request) returns (FetchProviderPackage.Response);
rpc ModulePackageVersions(ModulePackageVersions.Request) returns (ModulePackageVersions.Response);
rpc ModulePackageSourceAddr(ModulePackageSourceAddr.Request) returns (ModulePackageSourceAddr.Response);
rpc FetchModulePackage(FetchModulePackage.Request) returns (FetchModulePackage.Response);
}
message ProviderPackageVersions {
message Request {
string source_addr = 1;
}
message Response {
repeated string versions = 1;
repeated Diagnostic diagnostics = 2;
}
}
message FetchProviderPackage {
message Request {
string cache_dir = 1;
string source_addr = 2;
string version = 3;
repeated string platforms = 4;
repeated string hashes = 5;
}
message Response {
// Each requested platform will return a result in this list. The order
// of the returned results will match the order of the requested
// platforms. If the binary for a given platform could not be downloaded
// there will still be an entry in the results with diagnostics
// explaining why.
repeated FetchProviderPackage.PlatformResult results = 1;
repeated Diagnostic diagnostics = 2;
}
message PlatformResult {
ProviderPackage provider = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ModulePackageVersions {
message Request {
string source_addr = 2;
}
message Response {
repeated string versions = 1;
repeated Diagnostic diagnostics = 2;
}
}
message ModulePackageSourceAddr {
message Request {
string source_addr = 1;
string version = 2;
}
message Response {
string url = 1;
repeated Diagnostic diagnostics = 2;
}
}
message FetchModulePackage {
message Request {
string cache_dir = 1;
string url = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}

@ -0,0 +1,4 @@
{
"terraform_provider_foo": ["h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0="],
"terraform_provider_bar": ["h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="]
}

@ -0,0 +1,2 @@
This is not a real provider executable. It's just here to give the packages
service something to install.

@ -0,0 +1,2 @@
This is not a real provider executable. It's just here to give the packages
service something to install.
Loading…
Cancel
Save