Skip to content

Commit

Permalink
Merge pull request #98 from tombuildsstuff/f/reintroduce-resource-id-…
Browse files Browse the repository at this point in the history
…parsers

reintroducing the Resource ID Parsers
  • Loading branch information
tombuildsstuff authored Dec 14, 2023
2 parents 36d204c + fd7fe5d commit 9fa5a68
Show file tree
Hide file tree
Showing 47 changed files with 5,446 additions and 53 deletions.
155 changes: 155 additions & 0 deletions storage/2020-08-04/blob/accounts/resource_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package accounts

import (
"fmt"
"net/url"
"strings"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids"
)

type SubDomainType string

const (
BlobSubDomainType SubDomainType = "blob"
DataLakeStoreSubDomainType SubDomainType = "dfs"
FileSubDomainType SubDomainType = "file"
QueueSubDomainType SubDomainType = "queue"
TableSubDomainType SubDomainType = "table"
)

func PossibleValuesForSubDomainType() []SubDomainType {
return []SubDomainType{
BlobSubDomainType,
DataLakeStoreSubDomainType,
FileSubDomainType,
QueueSubDomainType,
TableSubDomainType,
}
}

// TODO: update this to implement `resourceids.ResourceId` once
// https://github.com/hashicorp/go-azure-helpers/issues/187 is fixed
var _ resourceids.Id = AccountId{}

type AccountId struct {
AccountName string
ZoneName *string
SubDomainType SubDomainType
DomainSuffix string
IsEdgeZone bool
}

func (a AccountId) ID() string {
components := []string{
a.AccountName,
}
if a.IsEdgeZone {
// Storage Accounts hosted in an Edge Zone
// `{accountname}.{component}.{edgezone}.edgestorage.azure.net`
components = append(components, string(a.SubDomainType))
if a.ZoneName != nil {
components = append(components, *a.ZoneName)
}
} else {
// Storage Accounts using a DNS Zone
// `{accountname}.{dnszone}.{component}.storage.azure.net`
// or a Regular Storage Account
// `{accountname}.{component}.core.windows.net`
if a.ZoneName != nil {
components = append(components, *a.ZoneName)
}
components = append(components, string(a.SubDomainType))
}
components = append(components, a.DomainSuffix)
return fmt.Sprintf("https://%s", strings.Join(components, "."))
}

func (a AccountId) String() string {
components := []string{
fmt.Sprintf("IsEdgeZone %t", a.IsEdgeZone),
fmt.Sprintf("ZoneName %q", pointer.From(a.ZoneName)),
fmt.Sprintf("Subdomain Type %q", string(a.SubDomainType)),
fmt.Sprintf("DomainSuffix %q", a.DomainSuffix),
}
return fmt.Sprintf("Account %q (%s)", a.AccountName, strings.Join(components, " / "))
}

func ParseAccountID(input, domainSuffix string) (*AccountId, error) {
uri, err := url.Parse(input)
if err != nil {
return nil, fmt.Errorf("parsing %q as a URL: %s", input, err)
}

if !strings.HasSuffix(uri.Host, domainSuffix) {
return nil, fmt.Errorf("expected the account %q to use a domain suffix of %q", uri.Host, domainSuffix)
}

// There's 3 different types of Storage Account ID:
// 1. Regular ol' Storage Accounts
// `{name}.{component}.core.windows.net` (e.g. `example1.blob.core.windows.net`)
// 2. Storage Accounts using a DNS Zone
// `{accountname}.{dnszone}.{component}.storage.azure.net`
// 3. Storage Accounts hosted in an Edge Zone
// `{accountname}.{component}.{edgezone}.edgestorage.azure.net`
// since both `dnszone` and `edgezone` are the only two identifiers, we need to check if `domainSuffix` includes `edge`
// to know how to treat these

hostName := strings.TrimSuffix(uri.Host, fmt.Sprintf(".%s", domainSuffix))
components := strings.Split(hostName, ".")
accountId := AccountId{
DomainSuffix: domainSuffix,
IsEdgeZone: strings.Contains(strings.ToLower(domainSuffix), "edge"),
}

if len(components) == 2 {
// this will be a regular Storage Account (e.g. `example1.blob.core.windows.net`)
accountId.AccountName = components[0]
subDomainType, err := parseSubDomainType(components[1])
if err != nil {
return nil, err
}
accountId.SubDomainType = *subDomainType
return &accountId, nil
}

if len(components) == 3 {
// This can either be a Zone'd Storage Account or a Storage Account within an Edge Zone
accountName := ""
subDomainTypeRaw := ""
zone := ""
if accountId.IsEdgeZone {
// `{accountname}.{component}.{edgezone}.edgestorage.azure.net`
accountName = components[0]
subDomainTypeRaw = components[1]
zone = components[2]
} else {
// `{accountname}.{dnszone}.{component}.storage.azure.net`
accountName = components[0]
zone = components[1]
subDomainTypeRaw = components[2]
}

accountId.AccountName = accountName
subDomainType, err := parseSubDomainType(subDomainTypeRaw)
if err != nil {
return nil, err
}
accountId.SubDomainType = *subDomainType
accountId.ZoneName = pointer.To(zone)
return &accountId, nil
}

return nil, fmt.Errorf("unknown storage account domain type %q", input)
}

func parseSubDomainType(input string) (*SubDomainType, error) {
for _, k := range PossibleValuesForSubDomainType() {
if strings.EqualFold(input, string(k)) {
return pointer.To(k), nil
}
}

return nil, fmt.Errorf("expected the subdomain type to be one of [%+v] but got %q", PossibleValuesForSubDomainType(), input)
}
133 changes: 133 additions & 0 deletions storage/2020-08-04/blob/accounts/resource_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package accounts

import (
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
)

func TestParseAccountIDStandard(t *testing.T) {
input := "https://example.blob.core.windows.net"
expected := AccountId{
AccountName: "example",
SubDomainType: BlobSubDomainType,
DomainSuffix: "core.windows.net",
}
actual, err := ParseAccountID(input, "core.windows.net")
if err != nil {
t.Fatalf(err.Error())
}
if actual.AccountName != expected.AccountName {
t.Fatalf("expected AccountName to be %q but got %q", expected.AccountName, actual.AccountName)
}
if actual.SubDomainType != expected.SubDomainType {
t.Fatalf("expected SubDomainType to be %q but got %q", expected.SubDomainType, actual.SubDomainType)
}
if actual.DomainSuffix != expected.DomainSuffix {
t.Fatalf("expected DomainSuffix to be %q but got %q", expected.DomainSuffix, actual.DomainSuffix)
}
}

func TestParseAccountIDInDNSZone(t *testing.T) {
input := "https://example.zone.blob.storage.azure.net"
expected := AccountId{
AccountName: "example",
ZoneName: pointer.To("zone"),
SubDomainType: BlobSubDomainType,
DomainSuffix: "storage.azure.net",
}
actual, err := ParseAccountID(input, "storage.azure.net")
if err != nil {
t.Fatalf(err.Error())
}
if actual.AccountName != expected.AccountName {
t.Fatalf("expected AccountName to be %q but got %q", expected.AccountName, actual.AccountName)
}
if actual.ZoneName == nil {
t.Fatalf("expected ZoneName to have a value but got nil")
}
if *actual.ZoneName != *expected.ZoneName {
t.Fatalf("expected ZoneName to be %q but got %q", *expected.ZoneName, *actual.ZoneName)
}
if actual.SubDomainType != expected.SubDomainType {
t.Fatalf("expected SubDomainType to be %q but got %q", expected.SubDomainType, actual.SubDomainType)
}
if actual.DomainSuffix != expected.DomainSuffix {
t.Fatalf("expected DomainSuffix to be %q but got %q", expected.DomainSuffix, actual.DomainSuffix)
}
if actual.IsEdgeZone {
t.Fatalf("expected IsEdgeZone to be false but got %t", actual.IsEdgeZone)
}
}

func TestParseAccountIDInEdgeZone(t *testing.T) {
input := "https://example.blob.danger.edgestorage.azure.net"
expected := AccountId{
AccountName: "example",
ZoneName: pointer.To("danger"),
IsEdgeZone: true,
SubDomainType: BlobSubDomainType,
DomainSuffix: "edgestorage.azure.net",
}
actual, err := ParseAccountID(input, "edgestorage.azure.net")
if err != nil {
t.Fatalf(err.Error())
}
if actual.AccountName != expected.AccountName {
t.Fatalf("expected AccountName to be %q but got %q", expected.AccountName, actual.AccountName)
}
if actual.ZoneName == nil {
t.Fatalf("expected ZoneName to have a value but got nil")
}
if *actual.ZoneName != *expected.ZoneName {
t.Fatalf("expected ZoneName to be %q but got %q", *expected.ZoneName, *actual.ZoneName)
}
if actual.SubDomainType != expected.SubDomainType {
t.Fatalf("expected SubDomainType to be %q but got %q", expected.SubDomainType, actual.SubDomainType)
}
if !actual.IsEdgeZone {
t.Fatalf("expected IsEdgeZone to be true but got %t", actual.IsEdgeZone)
}
}

func TestFormatAccountIDStandard(t *testing.T) {
actual := AccountId{
AccountName: "example1",
ZoneName: nil,
SubDomainType: FileSubDomainType,
DomainSuffix: "core.windows.net",
IsEdgeZone: false,
}.ID()
expected := "https://example1.file.core.windows.net"
if actual != expected {
t.Fatalf("expected %q but got %q", expected, actual)
}
}

func TestFormatAccountIDInDNSZone(t *testing.T) {
actual := AccountId{
AccountName: "example1",
ZoneName: pointer.To("zone1"),
SubDomainType: FileSubDomainType,
DomainSuffix: "storage.azure.net",
IsEdgeZone: false,
}.ID()
expected := "https://example1.zone1.file.storage.azure.net"
if actual != expected {
t.Fatalf("expected %q but got %q", expected, actual)
}
}

func TestFormatAccountIDInEdgeZone(t *testing.T) {
actual := AccountId{
AccountName: "example1",
ZoneName: pointer.To("zone1"),
SubDomainType: FileSubDomainType,
DomainSuffix: "edgestorage.azure.net",
IsEdgeZone: true,
}.ID()
expected := "https://example1.file.zone1.edgestorage.azure.net"
if actual != expected {
t.Fatalf("expected %q but got %q", expected, actual)
}
}
83 changes: 83 additions & 0 deletions storage/2020-08-04/blob/blobs/resource_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package blobs

import (
"fmt"
"net/url"
"strings"

"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids"
"github.com/tombuildsstuff/giovanni/storage/2020-08-04/blob/accounts"
)

// TODO: update this to implement `resourceids.ResourceId` once
// https://github.com/hashicorp/go-azure-helpers/issues/187 is fixed
var _ resourceids.Id = BlobId{}

type BlobId struct {
// AccountId specifies the ID of the Storage Account where this Blob exists.
AccountId accounts.AccountId

// ContainerName specifies the name of the Container within this Storage Account where this
// Blob exists.
ContainerName string

// BlobName specifies the name of this Blob.
BlobName string
}

func NewBlobID(accountId accounts.AccountId, containerName, blobName string) BlobId {
return BlobId{
AccountId: accountId,
ContainerName: containerName,
BlobName: blobName,
}
}

func (b BlobId) ID() string {
return fmt.Sprintf("%s/%s/%s", b.AccountId.ID(), b.ContainerName, b.BlobName)
}

func (b BlobId) String() string {
components := []string{
fmt.Sprintf("Account %q", b.AccountId.String()),
fmt.Sprintf("Container Name %q", b.ContainerName),
}
return fmt.Sprintf("Blob %q (%s)", b.BlobName, strings.Join(components, " / "))
}

// ParseBlobID parses `input` into a Blob ID using a known `domainSuffix`
func ParseBlobID(input, domainSuffix string) (*BlobId, error) {
// example: https://foo.blob.core.windows.net/Bar/example.vhd
if input == "" {
return nil, fmt.Errorf("`input` was empty")
}

account, err := accounts.ParseAccountID(input, domainSuffix)
if err != nil {
return nil, fmt.Errorf("parsing account %q: %+v", input, err)
}

if account.SubDomainType != accounts.BlobSubDomainType {
return nil, fmt.Errorf("expected the subdomain type to be %q but got %q", string(accounts.BlobSubDomainType), string(account.SubDomainType))
}

uri, err := url.Parse(input)
if err != nil {
return nil, fmt.Errorf("parsing %q as a uri: %+v", input, err)
}

path := strings.TrimPrefix(uri.Path, "/")
segments := strings.Split(path, "/")
if len(segments) != 2 {
return nil, fmt.Errorf("expected the path to contain 2 segments but got %d", len(segments))
}

containerName := segments[0]
blobName := strings.TrimPrefix(path, containerName)
blobName = strings.TrimPrefix(blobName, "/")
return &BlobId{
AccountId: *account,
ContainerName: containerName,
BlobName: blobName,
}, nil
}
Loading

0 comments on commit 9fa5a68

Please sign in to comment.