Skip to content

Instantly share code, notes, and snippets.

@Praveen005
Last active July 8, 2024 08:47
Show Gist options
  • Save Praveen005/eabe81a92b6d679172701fe030f667b4 to your computer and use it in GitHub Desktop.
Save Praveen005/eabe81a92b6d679172701fe030f667b4 to your computer and use it in GitHub Desktop.
Feature requested in #246 for terraform-provider-civo
package civo
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"github.com/civo/civogo"
"github.com/civo/terraform-provider-civo/civo/database"
"github.com/civo/terraform-provider-civo/civo/disk"
"github.com/civo/terraform-provider-civo/civo/dns"
"github.com/civo/terraform-provider-civo/civo/firewall"
"github.com/civo/terraform-provider-civo/civo/instances"
"github.com/civo/terraform-provider-civo/civo/ip"
"github.com/civo/terraform-provider-civo/civo/kubernetes"
"github.com/civo/terraform-provider-civo/civo/loadbalancer"
"github.com/civo/terraform-provider-civo/civo/network"
"github.com/civo/terraform-provider-civo/civo/objectstorage"
"github.com/civo/terraform-provider-civo/civo/region"
"github.com/civo/terraform-provider-civo/civo/size"
"github.com/civo/terraform-provider-civo/civo/ssh"
"github.com/civo/terraform-provider-civo/civo/volume"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
var (
// ProviderVersion is the version of the provider to set in the User-Agent header
ProviderVersion = "dev"
// ProdAPI is the Base URL for CIVO Production API
ProdAPI = "https://api.civo.com"
)
// Provider Civo cloud provider
func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
//------------change 1: replaced civo_token with credential_file field-----------------
"credential_file": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("CIVO_CREDENTIAL_FILE", ""),
Description: "Path to the Civo credentials file. Can be specified using CIVO_CREDENTIAL_FILE environment variable.",
},
//------------change 1 ends--------------------------------------------------------------
"region": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("CIVO_REGION", ""),
Description: "If region is not set, then no region will be used and them you need expensify in every resource even if you expensify here you can overwrite in a resource.",
},
"api_endpoint": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("CIVO_API_URL", ProdAPI),
Description: "The Base URL to use for CIVO API.",
},
},
DataSourcesMap: map[string]*schema.Resource{
// "civo_template": dataSourceTemplate(),
"civo_disk_image": disk.DataSourceDiskImage(),
"civo_kubernetes_version": kubernetes.DataSourceKubernetesVersion(),
"civo_kubernetes_cluster": kubernetes.DataSourceKubernetesCluster(),
"civo_size": size.DataSourceSize(),
"civo_instances": instances.DataSourceInstances(),
"civo_instance": instances.DataSourceInstance(),
"civo_dns_domain_name": dns.DataSourceDNSDomainName(),
"civo_dns_domain_record": dns.DataSourceDNSDomainRecord(),
"civo_network": network.DataSourceNetwork(),
"civo_volume": volume.DataSourceVolume(),
"civo_firewall": firewall.DataSourceFirewall(),
"civo_loadbalancer": loadbalancer.DataSourceLoadBalancer(),
"civo_ssh_key": ssh.DataSourceSSHKey(),
"civo_object_store": objectstorage.DataSourceObjectStore(),
"civo_object_store_credential": objectstorage.DataSourceObjectStoreCredential(),
"civo_region": region.DataSourceRegion(),
"civo_reserved_ip": ip.DataSourceReservedIP(),
"civo_database": database.DataSourceDatabase(),
"civo_database_version": database.DataDatabaseVersion(),
},
ResourcesMap: map[string]*schema.Resource{
"civo_instance": instances.ResourceInstance(),
"civo_instance_reserved_ip_assignment": instances.ResourceInstanceReservedIPAssignment(),
"civo_network": network.ResourceNetwork(),
"civo_volume": volume.ResourceVolume(),
"civo_volume_attachment": volume.ResourceVolumeAttachment(),
"civo_dns_domain_name": dns.ResourceDNSDomainName(),
"civo_dns_domain_record": dns.ResourceDNSDomainRecord(),
"civo_firewall": firewall.ResourceFirewall(),
"civo_ssh_key": ssh.ResourceSSHKey(),
"civo_kubernetes_cluster": kubernetes.ResourceKubernetesCluster(),
"civo_kubernetes_node_pool": kubernetes.ResourceKubernetesClusterNodePool(),
"civo_reserved_ip": ip.ResourceReservedIP(),
"civo_object_store": objectstorage.ResourceObjectStore(),
"civo_object_store_credential": objectstorage.ResourceObjectStoreCredential(),
"civo_database": database.ResourceDatabase(),
},
ConfigureFunc: providerConfigure,
}
}
// Provider configuration
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
var regionValue, tokenValue, apiURL string
var client *civogo.Client
var err error
if region, ok := d.GetOk("region"); ok {
regionValue = region.(string)
}
//---------change 2: calling getToken() to retrieve token-------------------------
if token, ok := getToken(d); ok {
tokenValue = token.(string)
} else {
return nil, fmt.Errorf("[ERR] token not found")
}
//--------------------------chnage 2 ends-------------------------------------------
if apiEndpoint, ok := d.GetOk("api_endpoint"); ok {
apiURL = apiEndpoint.(string)
} else {
apiURL = ProdAPI
}
client, err = civogo.NewClientWithURL(tokenValue, apiURL, regionValue)
if err != nil {
return nil, err
}
userAgent := &civogo.Component{
Name: "terraform-provider-civo",
Version: ProviderVersion,
}
client.SetUserAgent(userAgent)
log.Printf("[DEBUG] Civo API URL: %s\n", apiURL)
return client, nil
}
//-------------change 3: getToken() function definition and other helper functions---------------------------------
func getToken(d *schema.ResourceData) (interface{}, bool) {
var exists = true
// Check for CIVO_TOKEN environment variable
if token := os.Getenv("CIVO_TOKEN"); token != "" {
return token, exists
}
// Check for credentials file specified in provider config
if credFile, ok := d.GetOk("credential_file"); ok {
token, err := readTokenFromFile(credFile.(string))
if err == nil {
return token, exists
}
}
// Check for default CLI config file
homeDir, err := getHomeDir()
if err == nil {
token, err := readTokenFromFile(filepath.Join(homeDir, ".civo.json"))
if err == nil {
return token, exists
}
}
return nil, !exists
}
var getHomeDir = func() (string, error) {
if home := os.Getenv("HOME"); home != "" {
return home, nil
}
// Fall back to os.UserHomeDir() if HOME is not set
return os.UserHomeDir()
}
func readTokenFromFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
var config struct {
APIKeys struct {
Token string `json:"CIVO_TOKEN"`
} `json:"apikeys"`
}
if err := json.Unmarshal(data, &config); err != nil {
return "", err
}
return config.APIKeys.Token, nil
}
# Set the variable value in *.tfvars file or using -var="credential_file=..." CLI flag
#------change 1----------------
variable "credential_file" {}
#------change 1 ends----------------
# Specify required provider as maintained by civo
terraform {
required_providers {
civo = {
source = "civo/civo"
}
}
}
# Configure the Civo Provider
provider "civo" {
#------change 2----------------
credential_file = var.credential_file
#------change 2 ends----------------
region = "LON1"
}
# Create a web server
resource "civo_instance" "web" {
# ...
}
package civo
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
// TestProvider tests the provider configuration
func TestProvider(t *testing.T) {
if err := Provider().InternalValidate(); err != nil {
t.Fatalf("err: %s", err.Error())
}
}
// TestProvider_impl tests the provider implementation
func TestProvider_impl(t *testing.T) {
var _ *schema.Provider = Provider()
}
// TestToken tests the token configuration
func TestToken(t *testing.T) {
t.Run("reading token from environment variable", func(t *testing.T) {
const testToken = "env12345"
oldToken := os.Getenv("CIVO_TOKEN")
os.Setenv("CIVO_TOKEN", testToken)
defer os.Setenv("CIVO_TOKEN", oldToken) // Restore the original value
raw := map[string]interface{}{}
configureProvider(t, raw)
})
t.Run("reading token from credential file", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "civo-provider-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
credentialFile := filepath.Join(tempDir, "credential.json")
testToken := "file3409"
credContent := fmt.Sprintf(`{"apikeys":{"CIVO_TOKEN":"%s"}}`, testToken)
err = os.WriteFile(credentialFile, []byte(credContent), 0600)
if err != nil {
t.Fatalf("Failed to write credentials file: %v", err)
}
raw := map[string]interface{}{
"credential_file": credentialFile,
}
configureProvider(t, raw)
})
t.Run("reading token from CLI config", func(t *testing.T) {
// Create a mock CLI config file
tempDir, err := os.MkdirTemp("", "civo-cli-config-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
testToken := "12345"
cliConfigContent := fmt.Sprintf(`
{
"apikeys": {
"CIVO_TOKEN": "%s"
},
"meta": {
"admin": false,
"current_apikey": "CIVO_TOKEN",
"default_region": "NYC1",
"latest_release_check": "2024-07-07T14:07:51.996201195+05:30",
"url": "https://api.civo.com",
"last_command_executed": "2024-06-20T15:09:00.212548723+05:30"
},
"region_to_features": null
}`, testToken)
cliConfigFile := filepath.Join(tempDir, ".civo.json")
err = os.WriteFile(cliConfigFile, []byte(cliConfigContent), 0600)
if err != nil {
t.Fatalf("Failed to write CLI config file: %v", err)
}
// Temporarily set HOME to our temp directory
oldHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", oldHome)
raw := map[string]interface{}{}
configureProvider(t, raw)
})
}
func diagnosticsToString(diags diag.Diagnostics) string {
diagsAsStrings := make([]string, len(diags))
for i, diag := range diags {
diagsAsStrings[i] = diag.Summary
}
return strings.Join(diagsAsStrings, "; ")
}
func configureProvider(t testing.TB, raw map[string]interface{}) {
t.Helper()
rawProvider := Provider()
diags := rawProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(raw))
if diags.HasError() {
t.Fatalf("provider configure failed: %s", diagnosticsToString(diags))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment