Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/Azure/azure-dev into preflight
Browse files Browse the repository at this point in the history
  • Loading branch information
hemarina committed Oct 14, 2024
2 parents e17c1db + 213b74f commit 932cba3
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 4 deletions.
1 change: 1 addition & 0 deletions .vscode/cspell.global.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ ignoreWords:
- tfstate
- tfvars
- traf
- unmanage
- useragent
- versioncontrol
- vmss
Expand Down
1 change: 1 addition & 0 deletions .vscode/cspell.misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ overrides:
- azdev
- myimage
- azureai
- entra
3 changes: 2 additions & 1 deletion cli/azd/internal/repository/app_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ func prjConfigFromDetect(
root string,
detect detectConfirm) (project.ProjectConfig, error) {
config := project.ProjectConfig{
Name: filepath.Base(root),
Name: LabelName(filepath.Base(root)),
Metadata: &project.ProjectMetadata{
Template: fmt.Sprintf("%s@%s", InitGenTemplateId, internal.VersionInfo().Version),
},
Expand Down Expand Up @@ -410,6 +410,7 @@ func prjConfigFromDetect(
if name == "." {
name = config.Name
}
name = LabelName(name)
config.Services[name] = &svc
}

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/repository/infra_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (i *Initializer) infraSpecFromDetect(
}

for _, svc := range detect.Services {
name := filepath.Base(svc.Path)
name := LabelName(filepath.Base(svc.Path))
serviceSpec := scaffold.ServiceSpec{
Name: name,
Port: -1,
Expand Down
106 changes: 106 additions & 0 deletions cli/azd/internal/repository/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package repository

import "strings"

//cspell:disable

// LabelName cleans up a string to be used as a RFC 1123 Label name.
// It does not enforce the 63 character limit.
//
// RFC 1123 Label name:
// - contain only lowercase alphanumeric characters or '-'
// - start with an alphanumeric character
// - end with an alphanumeric character
//
// Examples:
// - myproject, MYPROJECT -> myproject
// - myProject, myProjecT, MyProject, MyProjecT -> my-project
// - my.project, My.Project, my-project, My-Project -> my-project
func LabelName(name string) string {
hasSeparator, n := cleanAlphaNumeric(name)
if hasSeparator {
return labelNameFromSeparators(n)
}

return labelNameFromCasing(name)
}

//cspell:enable

// cleanAlphaNumeric removes non-alphanumeric characters from the name.
//
// It also returns whether the name uses word separators.
func cleanAlphaNumeric(name string) (hasSeparator bool, cleaned string) {
sb := strings.Builder{}
hasSeparator = false
for _, c := range name {
if isAsciiAlphaNumeric(c) {
sb.WriteRune(c)
} else if isSeparator(c) {
hasSeparator = true
sb.WriteRune(c)
}
}

return hasSeparator, sb.String()
}

func isAsciiAlphaNumeric(r rune) bool {
return ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}

func isSeparator(r rune) bool {
return r == '-' || r == '_' || r == '.'
}

func lowerCase(r rune) rune {
if 'A' <= r && r <= 'Z' {
r += 'a' - 'A'
}
return r
}

// Converts camel-cased or Pascal-cased names into lower-cased dash-separated names.
// Example: MyProject, myProject -> my-project
func labelNameFromCasing(name string) string {
result := strings.Builder{}
// previously seen upper-case character
prevUpperCase := -2 // -2 to avoid matching the first character

for i, c := range name {
if 'A' <= c && c <= 'Z' {
if prevUpperCase == i-1 { // handle runs of upper-case word
prevUpperCase = i
result.WriteRune(lowerCase(c))
continue
}

if i > 0 && i != len(name)-1 {
result.WriteRune('-')
}

prevUpperCase = i
}

if isAsciiAlphaNumeric(c) {
result.WriteRune(lowerCase(c))
}
}

return result.String()
}

// Converts all word-separated names into lower-cased dash-separated names.
// Examples: my.project, my_project, My-Project -> my-project
func labelNameFromSeparators(name string) string {
result := strings.Builder{}
for i, c := range name {
if isAsciiAlphaNumeric(c) {
result.WriteRune(lowerCase(c))
} else if i > 0 && i != len(name)-1 && isSeparator(c) {
result.WriteRune('-')
}
}

return result.String()
}
67 changes: 67 additions & 0 deletions cli/azd/internal/repository/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package repository

import (
"testing"
)

//cspell:disable

func TestLabelName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"Lowercase", "myproject", "myproject"},
{"Uppercase", "MYPROJECT", "myproject"},
{"MixedCase", "myProject", "my-project"},
{"MixedCaseEnd", "myProjecT", "my-project"},
{"TitleCase", "MyProject", "my-project"},
{"TitleCaseEnd", "MyProjecT", "my-project"},
{"WithDot", "my.project", "my-project"},
{"WithDotTitleCase", "My.Project", "my-project"},
{"WithHyphen", "my-project", "my-project"},
{"WithHyphenTitleCase", "My-Project", "my-project"},
{"StartWithNumber", "1myproject", "1myproject"},
{"EndWithNumber", "myproject2", "myproject2"},
{"MixedWithNumbers", "my2Project3", "my2-project3"},
{"SpecialCharacters", "my_project!@#", "my-project"},
{"EmptyString", "", ""},
{"OnlySpecialCharacters", "@#$%^&*", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LabelName(tt.input)
if result != tt.expected {
t.Errorf("LabelName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestLabelNameEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"SingleCharacter", "A", "a"},
{"TwoCharacters", "Ab", "ab"},
{"StartEndHyphens", "-abc-", "abc"},
{"LongString",
"ThisIsOneVeryLongStringThatExceedsTheSixtyThreeCharacterLimitForRFC1123LabelNames",
"this-is-one-very-long-string-that-exceeds-the-sixty-three-character-limit-for-rfc1123-label-names"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LabelName(tt.input)
if result != tt.expected {
t.Errorf("LabelName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

//cspell:enable
111 changes: 109 additions & 2 deletions schemas/alpha/azure.yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
"type": "object",
"title": "The infrastructure configuration used for the application",
"description": "Optional. Provides additional configuration for Azure infrastructure provisioning.",
"additionalProperties": true,
"additionalProperties": false,
"required": [
"provider"
],
"properties": {
"provider": {
"type": "string",
Expand All @@ -55,8 +58,29 @@
"type": "string",
"title": "Name of the default module within the Azure provisioning templates",
"description": "Optional. The name of the Azure provisioning module used when provisioning resources. (Default: main)"
},
"deploymentStacks": {
"$ref": "#/definitions/deploymentStacksConfig"
}
}
},
"allOf": [
{
"if": {
"not": {
"properties": {
"provider": {
"const": "bicep"
}
}
}
},
"then": {
"properties": {
"deploymentStacks": false
}
}
}
]
},
"services": {
"type": "object",
Expand Down Expand Up @@ -1052,6 +1076,89 @@
"required": [
"deployment"
]
},
"deploymentStacksConfig": {
"type": "object",
"title": "The deployment stack configuration used for the project.",
"additionalProperties": false,
"oneOf": [
{
"required": [
"actionOnUnmanage"
]
},
{
"required": [
"denySettings"
]
}
],
"properties": {
"actionOnUnmanage": {
"type": "object",
"title": "The action to take when when resources become unmanaged",
"description": "Defines the behavior of resources that are no longer managed after the Deployment stack is updated or deleted. Defaults to 'delete' for all resource scopes.",
"required": [
"resourceGroups",
"resources"
],
"properties": {
"resourceGroups": {
"type": "string",
"title": "Required. The action on unmanage setting for resource groups",
"description": "Specifies an action for a newly unmanaged resource. Delete will attempt to delete the resource from Azure. Detach will leave the resource in it's current state.",
"default": "delete",
"enum": [
"delete",
"detach"
]
},
"resources": {
"type": "string",
"title": "Required. The action on unmanage setting for resources",
"description": "Specifies an action for a newly unmanaged resource. Delete will attempt to delete the resource from Azure. Detach will leave the resource in it's current state.",
"default": "delete",
"enum": [
"delete",
"detach"
]
}
}
},
"denySettings": {
"type": "object",
"title": "The deny settings for the deployment stack",
"description": "Defines how resources deployed by the stack are locked. Defaults to 'none'.",
"required": [
"mode"
],
"properties": {
"mode": {
"type": "string",
"title": "Required. Mode that defines denied actions.",
"default": "none",
"enum": [
"none",
"denyDelete",
"denyWriteAndDelete"
]
},
"applyToChildScopes": {
"type": "boolean",
"title": "Whether the deny settings apply to child scopes.",
"description": "DenySettings will be applied to child resource scopes of every managed resource with a deny assignment."
},
"excludedActions": {
"type": "array",
"title": "List of role-based management operations that are excluded from the denySettings."
},
"excludedPrincipals": {
"type": "array",
"title": "List of Entra ID principal IDs excluded from the lock. Up to 5 principals are permitted."
}
}
}
}
}
}
}

0 comments on commit 932cba3

Please sign in to comment.