Skip to content

Commit

Permalink
Add secret support to Terraform Providers (backend updates) (#7695)
Browse files Browse the repository at this point in the history
# Description

Added backend updates for secret support to Terraform Providers
configuration. This includes processing secrets in recipeConfig under
specific Provider configurations and environment variables. It also
updates existing functions which add secrets to .gitConfig for private
terraform modules.

Design Document: radius-project/design-notes#47


## Type of change

- This pull request fixes a bug in Radius and has an approved issue
#6539 .


Fixes: #6539
  • Loading branch information
lakshmimsft authored Jul 13, 2024
1 parent ccc385f commit b183209
Show file tree
Hide file tree
Showing 22 changed files with 1,303 additions and 141 deletions.
16 changes: 16 additions & 0 deletions pkg/corerp/datamodel/recipe_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type RecipeConfigProperties struct {

// Env specifies the environment variables to be set during the Terraform Recipe execution.
Env EnvironmentVariables `json:"env,omitempty"`

// EnvSecrets represents the environment secrets for the recipe.
// The keys of the map are the names of the secrets, and the values are the references to the secrets.
EnvSecrets map[string]SecretReference `json:"envSecrets,omitempty"`
}

// TerraformConfigProperties - Configuration for Terraform Recipes. Controls how Terraform plans and applies templates as
Expand Down Expand Up @@ -64,4 +68,16 @@ type EnvironmentVariables struct {
type ProviderConfigProperties struct {
// AdditionalProperties represents the non-sensitive environment variables to be set for the recipe execution.
AdditionalProperties map[string]any `json:"additionalProperties,omitempty"`

// Secrets represents the secrets to be set for recipe execution in the current Provider configuration.
Secrets map[string]SecretReference `json:"secrets,omitempty"`
}

// SecretReference represents a reference to a secret.
type SecretReference struct {
// Source represents the Secret Store ID of the secret.
Source string `json:"source"`

// Key represents the key of the secret.
Key string `json:"key"`
}
11 changes: 5 additions & 6 deletions pkg/recipes/configloader/mock_secret_loader.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 45 additions & 11 deletions pkg/recipes/configloader/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package configloader

import (
"context"
"fmt"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials"
Expand All @@ -34,21 +35,54 @@ type secretsLoader struct {
ArmClientOptions *arm.ClientOptions
}

func (e *secretsLoader) LoadSecrets(ctx context.Context, secretStore string) (v20231001preview.SecretStoresClientListSecretsResponse, error) {
secretStoreID, err := resources.ParseResource(secretStore)
if err != nil {
return v20231001preview.SecretStoresClientListSecretsResponse{}, err
// LoadSecrets loads secrets from secret stores based on input map of provided secret store IDs and secret keys.
// It returns a map of secret data, where the keys are the secret store IDs and the values are maps of secret keys and their corresponding values.
func (e *secretsLoader) LoadSecrets(ctx context.Context, secretStoreIDResourceKeys map[string][]string) (secretData map[string]map[string]string, err error) {
for secretStoreID, secretKeys := range secretStoreIDResourceKeys {
secretStoreResourceID, err := resources.ParseResource(secretStoreID)
if err != nil {
return nil, err
}

client, err := v20231001preview.NewSecretStoresClient(secretStoreResourceID.RootScope(), &aztoken.AnonymousCredential{}, e.ArmClientOptions)
if err != nil {
return nil, err
}

// Retrieve the secrets from the secret store.
secrets, err := client.ListSecrets(ctx, secretStoreResourceID.Name(), map[string]any{}, nil)
if err != nil {
return nil, err
}

// Populate the secretData map.
secretData, err = populateSecretData(secretStoreID, secretKeys, &secrets)
if err != nil {
return nil, err
}
}

client, err := v20231001preview.NewSecretStoresClient(secretStoreID.RootScope(), &aztoken.AnonymousCredential{}, e.ArmClientOptions)
if err != nil {
return v20231001preview.SecretStoresClientListSecretsResponse{}, err
return secretData, nil
}

// populateSecretData is a helper function to populate secret data from a secret store.
func populateSecretData(secretStoreID string, secretKeys []string, secrets *v20231001preview.SecretStoresClientListSecretsResponse) (map[string]map[string]string, error) {
secretData := make(map[string]map[string]string)

if secrets == nil {
return nil, fmt.Errorf("secrets not found for secret store ID '%s'", secretStoreID)
}

secrets, err := client.ListSecrets(ctx, secretStoreID.Name(), map[string]any{}, nil)
if err != nil {
return v20231001preview.SecretStoresClientListSecretsResponse{}, err
for _, secretKey := range secretKeys {
if secretDataValue, ok := secrets.Data[secretKey]; ok {
if secretData[secretStoreID] == nil {
secretData[secretStoreID] = make(map[string]string)
}
secretData[secretStoreID][secretKey] = *secretDataValue.Value
} else {
return nil, fmt.Errorf("a secret key was not found in secret store '%s'", secretStoreID)
}
}

return secrets, nil
return secretData, nil
}
77 changes: 77 additions & 0 deletions pkg/recipes/configloader/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package configloader

import (
"testing"

"github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
"github.com/radius-project/radius/pkg/to"
"github.com/stretchr/testify/require"
)

func Test_populateSecretData(t *testing.T) {
tests := []struct {
name string
secretKeys []string
secrets *v20231001preview.SecretStoresClientListSecretsResponse
secretStoreID string
expectedSecrets map[string]map[string]string
expectError bool
expectedErrMsg string
}{
{
name: "success",
secretKeys: []string{"secretKey1", "secretKey2"},
secrets: &v20231001preview.SecretStoresClientListSecretsResponse{
SecretStoreListSecretsResult: v20231001preview.SecretStoreListSecretsResult{
Data: map[string]*v20231001preview.SecretValueProperties{
"secretKey1": {
Value: to.Ptr("secretValue1"),
},
"secretKey2": {
Value: to.Ptr("secretValue2"),
},
}},
},
secretStoreID: "testSecretStore",
expectedSecrets: map[string]map[string]string{
"testSecretStore": {
"secretKey1": "secretValue1",
"secretKey2": "secretValue2",
},
},
expectError: false,
},
{
name: "fail with nil secrets input",
secretKeys: []string{"secretKey1"},
secrets: nil,
secretStoreID: "testSecretStore",
expectedSecrets: nil,
expectError: true,
expectedErrMsg: "secrets not found for secret store ID 'testSecretStore'",
},
{
name: "missing secret key",
secretKeys: []string{"missingKey"},
secrets: &v20231001preview.SecretStoresClientListSecretsResponse{
SecretStoreListSecretsResult: v20231001preview.SecretStoreListSecretsResult{},
},
secretStoreID: "testSecretStore",
expectedSecrets: nil,
expectError: true,
expectedErrMsg: "a secret key was not found in secret store 'testSecretStore'",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secretData, err := populateSecretData(tt.secretStoreID, tt.secretKeys, tt.secrets)
if tt.expectError {
require.EqualError(t, err, tt.expectedErrMsg)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedSecrets, secretData)
}
})
}
}
3 changes: 1 addition & 2 deletions pkg/recipes/configloader/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package configloader
import (
"context"

"github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
"github.com/radius-project/radius/pkg/recipes"
)

Expand All @@ -32,5 +31,5 @@ type ConfigurationLoader interface {

//go:generate mockgen -typed -destination=./mock_secret_loader.go -package=configloader -self_package github.com/radius-project/radius/pkg/recipes/configloader github.com/radius-project/radius/pkg/recipes/configloader SecretsLoader
type SecretsLoader interface {
LoadSecrets(ctx context.Context, secretStore string) (v20231001preview.SecretStoresClientListSecretsResponse, error)
LoadSecrets(ctx context.Context, secretStoreIDs map[string][]string) (secretData map[string]map[string]string, err error)
}
65 changes: 50 additions & 15 deletions pkg/recipes/driver/gitconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,23 @@ import (
"fmt"
"net/url"
"os/exec"
"reflect"
"strings"

git "github.com/go-git/go-git/v5"
"github.com/radius-project/radius/pkg/corerp/api/v20231001preview"
)

// getGitURLWithSecrets returns the git URL with secrets information added.
func getGitURLWithSecrets(secrets v20231001preview.SecretStoresClientListSecretsResponse, url *url.URL) string {
func getGitURLWithSecrets(secrets map[string]string, url *url.URL) string {
// accessing the secret values and creating the git url with secret information.
var username, pat *string
path := fmt.Sprintf("%s://", url.Scheme)
user, ok := secrets.Data["username"]
user, ok := secrets["username"]
if ok {
username = user.Value
path += fmt.Sprintf("%s:", *username)
path += fmt.Sprintf("%s:", user)
}

token, ok := secrets.Data["pat"]
token, ok := secrets["pat"]
if ok {
pat = token.Value
path += *pat
path += token
}
path += fmt.Sprintf("@%s", url.Hostname())

Expand All @@ -52,7 +47,7 @@ func getGitURLWithSecrets(secrets v20231001preview.SecretStoresClientListSecrets
// getURLConfigKeyValue is used to get the key and value details of the url config.
// get the secret values pat and username from secrets and create a git url in
// the format : https://<username>:<pat>@<git>.com
func getURLConfigKeyValue(secrets v20231001preview.SecretStoresClientListSecretsResponse, templatePath string) (string, string, error) {
func getURLConfigKeyValue(secrets map[string]string, templatePath string) (string, string, error) {
url, err := GetGitURL(templatePath)
if err != nil {
return "", "", err
Expand All @@ -71,8 +66,8 @@ func getURLConfigKeyValue(secrets v20231001preview.SecretStoresClientListSecrets
// Retrieves the git credentials from the provided secrets object
// and adds them to the Git config by running
// git config --file .git/config url<template_path_domain_with_credentails>.insteadOf <template_path_domain>.
func addSecretsToGitConfig(workingDirectory string, secrets v20231001preview.SecretStoresClientListSecretsResponse, templatePath string) error {
if !strings.HasPrefix(templatePath, "git::") || reflect.DeepEqual(secrets, v20231001preview.SecretStoresClientListSecretsResponse{}) {
func addSecretsToGitConfig(workingDirectory string, secrets map[string]string, templatePath string) error {
if !strings.HasPrefix(templatePath, "git::") || secrets == nil || len(secrets) == 0 {
return nil
}

Expand Down Expand Up @@ -118,8 +113,8 @@ func setGitConfigForDir(workingDirectory string) error {
// unsetGitConfigForDir removes a conditional include directive from the global Git configuration.
// This function modifies the global Git configuration to remove a previously set `includeIf` directive
// for a given working directory.
func unsetGitConfigForDir(workingDirectory string, secrets v20231001preview.SecretStoresClientListSecretsResponse, templatePath string) error {
if !strings.HasPrefix(templatePath, "git::") || reflect.DeepEqual(secrets, v20231001preview.SecretStoresClientListSecretsResponse{}) {
func unsetGitConfigForDir(workingDirectory string, secrets map[string]string, templatePath string) error {
if !strings.HasPrefix(templatePath, "git::") || secrets == nil || len(secrets) == 0 {
return nil
}

Expand Down Expand Up @@ -149,3 +144,43 @@ func GetGitURL(templatePath string) (*url.URL, error) {

return url, nil
}

// addSecretsToGitConfigIfApplicable adds secrets to the Git configuration file if applicable.
// It is a wrapper function to addSecretsToGitConfig()
func addSecretsToGitConfigIfApplicable(secretStoreID string, secretData map[string]map[string]string, requestDirPath string, templatePath string) error {
if secretStoreID == "" || secretData == nil {
return nil
}

secrets, ok := secretData[secretStoreID]
if !ok {
return fmt.Errorf("secrets not found for secret store ID %q", secretStoreID)
}

err := addSecretsToGitConfig(requestDirPath, secrets, templatePath)
if err != nil {
return err
}

return nil
}

// unsetGitConfigForDir removes a conditional include directive from the global Git configuration if applicable.
// It is a wrapper function to unsetGitConfigForDir()
func unsetGitConfigForDirIfApplicable(secretStoreID string, secretData map[string]map[string]string, requestDirPath string, templatePath string) error {
if secretStoreID == "" || secretData == nil {
return nil
}

secrets, ok := secretData[secretStoreID]
if !ok {
return fmt.Errorf("secrets not found for secret store ID %q", secretStoreID)
}

err := unsetGitConfigForDir(requestDirPath, secrets, templatePath)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit b183209

Please sign in to comment.