Skip to content

Commit

Permalink
feat: install multiple providers from YAML file
Browse files Browse the repository at this point in the history
  • Loading branch information
Madh93 committed Apr 25, 2023
1 parent dc961cf commit 1dcc670
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 10 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ You can also specify the architecture and operating system. If not specified, th

<img alt="Install a provider" src="docs/gif/install.gif"/>

In addition, it's possible to install multiple providers at once specifying a `providers.yml` file, making it easier to share and reuse installation requirements. For example:

```yaml
providers:
- name: hashicorp/[email protected]
- name: hashicorp/random
os:
- linux
- darwin
arch:
- amd64
- arm64
```
<img alt="Install providers from file" src="docs/gif/install-from-file.gif"/>
### List installed providers
This will display on the screen the installed providers. Optionally, you can specify an output format. Valid output formats are:
Expand Down Expand Up @@ -119,7 +135,6 @@ This will delete all installed providers from the current registry.
- [Terraform plugin caching](https://www.scalefactory.com/blog/2021/02/25/terraform-plugin-caching/)
- [How to Speed Up Terraform in CI/CD Pipelines](https://infinitelambda.com/speed-up-terraform-cicd-pipeline/)


## License

This project is licensed under the [MIT license](LICENSE).
34 changes: 26 additions & 8 deletions cmd/install.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"log"
"runtime"

Expand All @@ -13,18 +14,34 @@ var installCmd = &cobra.Command{
Use: "install [provider]",
Aliases: []string{"i"},
Short: "Install a provider",
Args: cobra.ExactArgs(1),
Args: func(cmd *cobra.Command, args []string) error {
installFromFile := getStringFlag(cmd, "from-file")
if len(args) != 1 && installFromFile == "" {
return fmt.Errorf("requires 1 arg when '--from-file' flag is not passed, received %d", len(args))
}
if len(args) >= 1 && installFromFile != "" {
return fmt.Errorf("requires 0 arg when '--from-file' flag is passed, received %d", len(args))
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
var providers []*terraform.Provider
var err error

// Get providers to install
for _, os := range getStringSliceFlag(cmd, "os") {
for _, arch := range getStringSliceFlag(cmd, "arch") {
providerName, err := terraform.ParseProviderName(args[0])
if err != nil {
log.Fatal("Error: ", err)
if getStringFlag(cmd, "from-file") != "" {
providers, err = tpm.ParseProvidersFromFile(getStringFlag(cmd, "from-file"))
if err != nil {
log.Fatal("Error: ", err)
}
} else {
for _, os := range getStringSliceFlag(cmd, "os") {
for _, arch := range getStringSliceFlag(cmd, "arch") {
providerName, err := terraform.ParseProviderName(args[0])
if err != nil {
log.Fatal("Error: ", err)
}
providers = append(providers, terraform.NewProvider(providerName, os, arch))
}
providers = append(providers, terraform.NewProvider(providerName, os, arch))
}
}

Expand All @@ -43,6 +60,7 @@ func init() {

// Local Flags
installCmd.Flags().Bool("force", false, "forces the installation of the provider even if it already exists")
installCmd.Flags().StringP("from-file", "f", "", "installs providers defined in a 'providers.yml' file")
installCmd.Flags().StringSliceP("os", "o", []string{runtime.GOOS}, "terraform provider operating system")
installCmd.Flags().StringSliceP("arch", "a", []string{runtime.GOARCH}, "terraform provider architecture")
}
5 changes: 5 additions & 0 deletions cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ func bindCustomFlag(flag *pflag.Flag) {
viper.BindPFlag(name, flag)
}

func getStringFlag(cmd *cobra.Command, flag string) (value string) {
value, _ = cmd.Flags().GetString(flag)
return
}

func getStringSliceFlag(cmd *cobra.Command, flag string) (value []string) {
value, _ = cmd.Flags().GetStringSlice(flag)
return
Expand Down
Binary file added docs/gif/install-from-file.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/gif/install-from-file.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Output install-from-file.gif

Set FontSize 20
Set Width 1200
Set Height 600

Type "tpm install --from-file examples/01-basic.yml"
Sleep 500ms
Enter
Sleep 4s

Ctrl+L
Type "tpm install -f examples/02-multi-os-arch.yml"
Sleep 500ms
Enter
Sleep 6s
4 changes: 4 additions & 0 deletions examples/01-basic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
providers:
- name: hashicorp/[email protected]
- name: hashicorp/null@latest
- name: hashicorp/random
13 changes: 13 additions & 0 deletions examples/02-multi-os-arch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
providers:
- name: hashicorp/null@latest
os:
- windows
arch:
- amd64
- name: hashicorp/random
os:
- linux
- darwin
arch:
- amd64
- arm64
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -29,5 +30,4 @@ require (
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
23 changes: 23 additions & 0 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package parser

import (
"fmt"
"path/filepath"

"github.com/Madh93/tpm/internal/terraform"
)

type InputParser interface {
Parse(input []byte) ([]*terraform.Provider, error)
}

func NewParser(path string) (InputParser, error) {
extension := filepath.Ext(path)

switch extension {
case ".yml", ".yaml":
return &YAMLParser{}, nil
}

return nil, fmt.Errorf("unsupported '%s' input format", extension)
}
46 changes: 46 additions & 0 deletions internal/parser/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package parser_test

import (
"testing"

"github.com/Madh93/tpm/internal/parser"
"github.com/stretchr/testify/assert"
)

func TestNewParser(t *testing.T) {
tests := []struct {
name string
path string
wantErr bool
}{
{
name: "valid 'yml' output format",
path: "providers.yml",
wantErr: false,
},
{
name: "valid 'yaml' output format",
path: "whatever.yaml",
wantErr: false,
},
{
name: "invalid input format",
path: "providers.json",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := parser.NewParser(tt.path)

if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, p)
} else {
assert.NoError(t, err)
assert.NotZero(t, p)
}
})
}
}
51 changes: 51 additions & 0 deletions internal/parser/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package parser

import (
"runtime"

"github.com/Madh93/tpm/internal/terraform"
"gopkg.in/yaml.v3"
)

type YAMLProvidersFile struct {
Providers []struct {
Name string `yaml:"name"`
OS []string `yaml:"os"`
Arch []string `yaml:"arch"`
} `yaml:"providers"`
}

type YAMLParser struct{}

func (f *YAMLParser) Parse(data []byte) (providers []*terraform.Provider, err error) {
// Decode YAML
var config YAMLProvidersFile
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}

// Parse providers
for _, provider := range config.Providers {
osList := getListOrDefault(provider.OS, []string{runtime.GOOS})
archList := getListOrDefault(provider.Arch, []string{runtime.GOARCH})
for _, os := range osList {
for _, arch := range archList {
providerName, err := terraform.ParseProviderName(provider.Name)
if err != nil {
return nil, err
}
providers = append(providers, terraform.NewProvider(providerName, os, arch))
}
}
}

return providers, nil
}

func getListOrDefault(list, fallback []string) []string {
if len(list) == 0 {
return fallback
}
return list
}
106 changes: 106 additions & 0 deletions internal/parser/yaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package parser_test

import (
"reflect"
"runtime"
"testing"

"github.com/Madh93/tpm/internal/parser"
"github.com/Madh93/tpm/internal/terraform"
"github.com/stretchr/testify/assert"
)

func TestYAMLParserParse(t *testing.T) {
tests := []struct {
name string
input string
expected []*terraform.Provider
}{
{
name: "no providers definition",
input: "",
expected: nil,
},
{
name: "one simple provider definition",
input: `providers:
- name: hashicorp/[email protected]`,
expected: []*terraform.Provider{terraform.NewProvider(terraform.NewProviderName("hashicorp", "http", "3.2.1"), runtime.GOOS, runtime.GOARCH)},
},
{
name: "one provider definition with multiple arch and os",
input: `providers:
- name: hashicorp/[email protected]
os:
- linux
- darwin
arch:
- amd64
- arm64`,
expected: []*terraform.Provider{
terraform.NewProvider(terraform.NewProviderName("hashicorp", "http", "3.2.1"), "linux", "amd64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "http", "3.2.1"), "linux", "arm64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "http", "3.2.1"), "darwin", "amd64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "http", "3.2.1"), "darwin", "arm64"),
},
},
{
name: "multiple providers definitions",
input: `providers:
- name: cloudflare/[email protected]
- name: digitalocean/[email protected]
- name: hashicorp/[email protected]`,
expected: []*terraform.Provider{
terraform.NewProvider(terraform.NewProviderName("cloudflare", "cloudflare", "4.4.0"), runtime.GOOS, runtime.GOARCH),
terraform.NewProvider(terraform.NewProviderName("digitalocean", "digitalocean", "2.28.0"), runtime.GOOS, runtime.GOARCH),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "aws", "4.64.0"), runtime.GOOS, runtime.GOARCH),
},
},
{
name: "multiple providers definitions with multiple arch and os",
input: `providers:
- name: cloudflare/[email protected]
os:
- darwin
arch:
- amd64
- arm64
- name: digitalocean/[email protected]
os:
- windows
- linux
arch:
- amd64
- name: hashicorp/[email protected]
os:
- linux
- darwin
arch:
- amd64
- arm64`,
expected: []*terraform.Provider{
terraform.NewProvider(terraform.NewProviderName("cloudflare", "cloudflare", "4.4.0"), "darwin", "amd64"),
terraform.NewProvider(terraform.NewProviderName("cloudflare", "cloudflare", "4.4.0"), "darwin", "arm64"),
terraform.NewProvider(terraform.NewProviderName("digitalocean", "digitalocean", "2.28.0"), "windows", "amd64"),
terraform.NewProvider(terraform.NewProviderName("digitalocean", "digitalocean", "2.28.0"), "linux", "amd64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "aws", "4.64.0"), "linux", "amd64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "aws", "4.64.0"), "linux", "arm64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "aws", "4.64.0"), "darwin", "amd64"),
terraform.NewProvider(terraform.NewProviderName("hashicorp", "aws", "4.64.0"), "darwin", "arm64"),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlParser := &parser.YAMLParser{}
providers, err := yamlParser.Parse([]byte(tt.input))

assert.NoError(t, err)
assert.Equal(t, len(tt.expected), len(providers))
if !reflect.DeepEqual(providers, tt.expected) {
t.Errorf("TestYAMLParserParse(%q): expected provider %v, but got %v", tt.input, tt.expected, providers)
}
})
}
}
Loading

0 comments on commit 1dcc670

Please sign in to comment.