Skip to content

Commit

Permalink
Avoid maintain env list in multiple place (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
rujche authored Nov 28, 2024
1 parent 817ce35 commit 40c305d
Show file tree
Hide file tree
Showing 17 changed files with 1,380 additions and 617 deletions.
2 changes: 1 addition & 1 deletion cli/azd/internal/appdetect/spring_boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func detectEventHubsAccordingToSpringCloudStreamBinderMavenDependency(

func detectEventHubsAccordingToSpringCloudStreamKafkaMavenDependency(
azdProject *Project, springBootProject *SpringBootProject) {
var targetGroupId = "com.azure.spring"
var targetGroupId = "org.springframework.cloud"
var targetArtifactId = "spring-cloud-starter-stream-kafka"
if hasDependency(springBootProject, targetGroupId, targetArtifactId) {
bindingDestinations := getBindingDestinationMap(springBootProject.applicationProperties)
Expand Down
214 changes: 214 additions & 0 deletions cli/azd/internal/scaffold/bicep_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package scaffold

import (
"fmt"
"github.com/azure/azure-dev/cli/azd/internal"
"strings"
)

func ToBicepEnv(env Env) BicepEnv {
if isResourceConnectionEnv(env.Value) {
resourceType, resourceInfoType := toResourceConnectionInfo(env.Value)
value, ok := bicepEnv[resourceType][resourceInfoType]
if !ok {
panic(unsupportedType(env))
}
if isSecret(resourceInfoType) {
if isKeyVaultSecret(value) {
return BicepEnv{
BicepEnvType: BicepEnvTypeKeyVaultSecret,
Name: env.Name,
SecretName: secretName(env),
SecretValue: unwrapKeyVaultSecretValue(value),
}
} else {
return BicepEnv{
BicepEnvType: BicepEnvTypeSecret,
Name: env.Name,
SecretName: secretName(env),
SecretValue: value,
}
}
} else {
return BicepEnv{
BicepEnvType: BicepEnvTypePlainText,
Name: env.Name,
PlainTextValue: value,
}
}
} else {
return BicepEnv{
BicepEnvType: BicepEnvTypePlainText,
Name: env.Name,
PlainTextValue: toBicepEnvPlainTextValue(env.Value),
}
}
}

func ShouldAddToBicepFile(spec ServiceSpec, name string) bool {
return !willBeAddedByServiceConnector(spec, name)
}

func willBeAddedByServiceConnector(spec ServiceSpec, name string) bool {
if (spec.DbPostgres != nil && spec.DbPostgres.AuthType == internal.AuthTypeUserAssignedManagedIdentity) ||
(spec.DbMySql != nil && spec.DbMySql.AuthType == internal.AuthTypeUserAssignedManagedIdentity) {
return name == "spring.datasource.url" ||
name == "spring.datasource.username" ||
name == "spring.datasource.azure.passwordless-enabled"
} else {
return false
}
}

// inputStringExample -> 'inputStringExample'
func addQuotation(input string) string {
return fmt.Sprintf("'%s'", input)
}

// 'inputStringExample' -> 'inputStringExample'
// '${inputSingleVariableExample}' -> inputSingleVariableExample
// '${HOST}:${PORT}' -> '${HOST}:${PORT}'
func removeQuotationIfItIsASingleVariable(input string) string {
prefix := "'${"
suffix := "}'"
if strings.HasPrefix(input, prefix) && strings.HasSuffix(input, suffix) {
prefixTrimmed := strings.TrimPrefix(input, prefix)
trimmed := strings.TrimSuffix(prefixTrimmed, suffix)
if strings.IndexAny(trimmed, "}") == -1 {
return trimmed
} else {
return input
}
} else {
return input
}
}

// The BicepEnv.PlainTextValue is handled as variable by default.
// If the value is string, it should contain (').
// Here are some examples of input and output:
// inputStringExample -> 'inputStringExample'
// ${inputSingleVariableExample} -> inputSingleVariableExample
// ${HOST}:${PORT} -> '${HOST}:${PORT}'
func toBicepEnvPlainTextValue(input string) string {
return removeQuotationIfItIsASingleVariable(addQuotation(input))
}

// BicepEnv
//
// For Name and SecretName, they are handled as string by default.
// Which means quotation will be added before they are used in bicep file, because they are always string value.
//
// For PlainTextValue and SecretValue, they are handled as variable by default.
// When they are string value, quotation should be contained by themselves.
// Set variable as default is mainly to avoid this problem:
// https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter-rule-simplify-interpolation
type BicepEnv struct {
BicepEnvType BicepEnvType
Name string
PlainTextValue string
SecretName string
SecretValue string
}

type BicepEnvType string

const (
BicepEnvTypePlainText BicepEnvType = "plainText"
BicepEnvTypeSecret BicepEnvType = "secret"
BicepEnvTypeKeyVaultSecret BicepEnvType = "keyVaultSecret"
)

// Note: The value is handled as variable.
// If the value is string, it should contain quotation inside itself.
var bicepEnv = map[ResourceType]map[ResourceInfoType]string{
ResourceTypeDbPostgres: {
ResourceInfoTypeHost: "postgreServer.outputs.fqdn",
ResourceInfoTypePort: "'5432'",
ResourceInfoTypeDatabaseName: "postgreSqlDatabaseName",
ResourceInfoTypeUsername: "postgreSqlDatabaseUser",
ResourceInfoTypePassword: "postgreSqlDatabasePassword",
ResourceInfoTypeUrl: "'postgresql://${postgreSqlDatabaseUser}:${postgreSqlDatabasePassword}@${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}'",
ResourceInfoTypeJdbcUrl: "'jdbc:postgresql://${postgreServer.outputs.fqdn}:5432/${postgreSqlDatabaseName}'",
},
ResourceTypeDbMySQL: {
ResourceInfoTypeHost: "mysqlServer.outputs.fqdn",
ResourceInfoTypePort: "'3306'",
ResourceInfoTypeDatabaseName: "mysqlDatabaseName",
ResourceInfoTypeUsername: "mysqlDatabaseUser",
ResourceInfoTypePassword: "mysqlDatabasePassword",
ResourceInfoTypeUrl: "'mysql://${mysqlDatabaseUser}:${mysqlDatabasePassword}@${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}'",
ResourceInfoTypeJdbcUrl: "'jdbc:mysql://${mysqlServer.outputs.fqdn}:3306/${mysqlDatabaseName}'",
},
ResourceTypeDbRedis: {
ResourceInfoTypeHost: "redis.outputs.hostName",
ResourceInfoTypePort: "string(redis.outputs.sslPort)",
ResourceInfoTypeEndpoint: "'${redis.outputs.hostName}:${redis.outputs.sslPort}'",
ResourceInfoTypePassword: wrapToKeyVaultSecretValue("redisConn.outputs.keyVaultUrlForPass"),
ResourceInfoTypeUrl: wrapToKeyVaultSecretValue("redisConn.outputs.keyVaultUrlForUrl"),
},
ResourceTypeDbMongo: {
ResourceInfoTypeDatabaseName: "mongoDatabaseName",
ResourceInfoTypeUrl: wrapToKeyVaultSecretValue("cosmos.outputs.exportedSecrets['MONGODB-URL'].secretUri"),
},
ResourceTypeDbCosmos: {
ResourceInfoTypeEndpoint: "cosmos.outputs.endpoint",
ResourceInfoTypeDatabaseName: "cosmosDatabaseName",
},
ResourceTypeMessagingServiceBus: {
ResourceInfoTypeNamespace: "serviceBusNamespace.outputs.name",
ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("serviceBusConnectionString.outputs.keyVaultUrl"),
},
ResourceTypeMessagingEventHubs: {
ResourceInfoTypeNamespace: "eventHubNamespace.outputs.name",
ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("eventHubsConnectionString.outputs.keyVaultUrl"),
},
ResourceTypeMessagingKafka: {
ResourceInfoTypeEndpoint: "'${eventHubNamespace.outputs.name}.servicebus.windows.net:9093'",
ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("eventHubsConnectionString.outputs.keyVaultUrl"),
},
ResourceTypeStorage: {
ResourceInfoTypeAccountName: "storageAccountName",
ResourceInfoTypeConnectionString: wrapToKeyVaultSecretValue("storageAccountConnectionString.outputs.keyVaultUrl"),
},
ResourceTypeOpenAiModel: {
ResourceInfoTypeEndpoint: "account.outputs.endpoint",
},
ResourceTypeHostContainerApp: {},
}

func unsupportedType(env Env) string {
return fmt.Sprintf("unsupported connection info type for resource type. "+
"value = %s", env.Value)
}

func PlaceHolderForServiceIdentityClientId() string {
return "__PlaceHolderForServiceIdentityClientId"
}

func isSecret(info ResourceInfoType) bool {
return info == ResourceInfoTypePassword || info == ResourceInfoTypeUrl || info == ResourceInfoTypeConnectionString
}

func secretName(env Env) string {
resourceType, resourceInfoType := toResourceConnectionInfo(env.Value)
name := fmt.Sprintf("%s-%s", resourceType, resourceInfoType)
lowerCaseName := strings.ToLower(name)
noDotName := strings.Replace(lowerCaseName, ".", "-", -1)
noUnderscoreName := strings.Replace(noDotName, "_", "-", -1)
return noUnderscoreName
}

var keyVaultSecretPrefix = "keyvault:"

func isKeyVaultSecret(value string) bool {
return strings.HasPrefix(value, keyVaultSecretPrefix)
}

func wrapToKeyVaultSecretValue(value string) string {
return fmt.Sprintf("%s%s", keyVaultSecretPrefix, value)
}

func unwrapKeyVaultSecretValue(value string) string {
return strings.TrimPrefix(value, keyVaultSecretPrefix)
}
172 changes: 172 additions & 0 deletions cli/azd/internal/scaffold/bicep_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package scaffold

import (
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/stretchr/testify/assert"
"testing"
)

func TestToBicepEnv(t *testing.T) {
tests := []struct {
name string
in Env
want BicepEnv
}{
{
name: "Plain text",
in: Env{
Name: "enable-customer-related-feature",
Value: "true",
},
want: BicepEnv{
BicepEnvType: BicepEnvTypePlainText,
Name: "enable-customer-related-feature",
PlainTextValue: "'true'", // Note: Quotation add automatically
},
},
{
name: "Plain text from EnvTypeResourceConnectionPlainText",
in: Env{
Name: "spring.jms.servicebus.pricing-tier",
Value: "premium",
},
want: BicepEnv{
BicepEnvType: BicepEnvTypePlainText,
Name: "spring.jms.servicebus.pricing-tier",
PlainTextValue: "'premium'", // Note: Quotation add automatically
},
},
{
name: "Plain text from EnvTypeResourceConnectionResourceInfo",
in: Env{
Name: "POSTGRES_PORT",
Value: ToResourceConnectionEnv(ResourceTypeDbPostgres, ResourceInfoTypePort),
},
want: BicepEnv{
BicepEnvType: BicepEnvTypePlainText,
Name: "POSTGRES_PORT",
PlainTextValue: "'5432'",
},
},
{
name: "Secret",
in: Env{
Name: "POSTGRES_PASSWORD",
Value: ToResourceConnectionEnv(ResourceTypeDbPostgres, ResourceInfoTypePassword),
},
want: BicepEnv{
BicepEnvType: BicepEnvTypeSecret,
Name: "POSTGRES_PASSWORD",
SecretName: "db-postgres-password",
SecretValue: "postgreSqlDatabasePassword",
},
},
{
name: "KeuVault Secret",
in: Env{
Name: "REDIS_PASSWORD",
Value: ToResourceConnectionEnv(ResourceTypeDbRedis, ResourceInfoTypePassword),
},
want: BicepEnv{
BicepEnvType: BicepEnvTypeKeyVaultSecret,
Name: "REDIS_PASSWORD",
SecretName: "db-redis-password",
SecretValue: "redisConn.outputs.keyVaultUrlForPass",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := ToBicepEnv(tt.in)
assert.Equal(t, tt.want, actual)
})
}
}

func TestToBicepEnvPlainTextValue(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "string",
in: "inputStringExample",
want: "'inputStringExample'",
},
{
name: "single variable",
in: "${inputSingleVariableExample}",
want: "inputSingleVariableExample",
},
{
name: "multiple variable",
in: "${HOST}:${PORT}",
want: "'${HOST}:${PORT}'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := toBicepEnvPlainTextValue(tt.in)
assert.Equal(t, tt.want, actual)
})
}
}

func TestShouldAddToBicepFile(t *testing.T) {
tests := []struct {
name string
infraSpec ServiceSpec
propertyName string
want bool
}{
{
name: "not related property and not using mysql and postgres",
infraSpec: ServiceSpec{},
propertyName: "test",
want: true,
},
{
name: "not using mysql and postgres",
infraSpec: ServiceSpec{},
propertyName: "spring.datasource.url",
want: true,
},
{
name: "not using user assigned managed identity",
infraSpec: ServiceSpec{
DbMySql: &DatabaseMySql{
AuthType: internal.AuthTypePassword,
},
},
propertyName: "spring.datasource.url",
want: true,
},
{
name: "not service connector added property",
infraSpec: ServiceSpec{
DbMySql: &DatabaseMySql{
AuthType: internal.AuthTypePassword,
},
},
propertyName: "test",
want: true,
},
{
name: "should not added",
infraSpec: ServiceSpec{
DbMySql: &DatabaseMySql{
AuthType: internal.AuthTypePassword,
},
},
propertyName: "spring.datasource.url",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := ShouldAddToBicepFile(tt.infraSpec, tt.propertyName)
assert.Equal(t, tt.want, actual)
})
}
}
Loading

0 comments on commit 40c305d

Please sign in to comment.