From 15bbb4db6b059bdfb1fb723f995df87b0d8db200 Mon Sep 17 00:00:00 2001 From: Samuel Karp Date: Thu, 10 Aug 2017 17:01:40 -0700 Subject: [PATCH] aws/arn: Package for parsing and producing ARNs --- aws/arn/arn.go | 86 +++++++++++++++++++++++++++++++++++++++++++ aws/arn/arn_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 aws/arn/arn.go create mode 100644 aws/arn/arn_test.go diff --git a/aws/arn/arn.go b/aws/arn/arn.go new file mode 100644 index 00000000000..44aa125a188 --- /dev/null +++ b/aws/arn/arn.go @@ -0,0 +1,86 @@ +// Package arn provides a parser for interacting with Amazon Resource Names. +package arn + +import ( + "errors" + "strings" +) + +const ( + arnDelimiter = ":" + arnSections = 6 + arnPrefix = "arn:" + + // zero-indexed + sectionPartition = 1 + sectionService = 2 + sectionRegion = 3 + sectionAccountID = 4 + sectionResource = 5 + + // errors + invalidPrefix = "arn: invalid prefix" + invalidSections = "arn: not enough sections" +) + +// ARN captures the individual fields of an Amazon Resource Name. +// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for more information. +type ARN struct { + // The partition that the resource is in. For standard AWS regions, the partition is "aws". If you have resources in + // other partitions, the partition is "aws-partitionname". For example, the partition for resources in the China + // (Beijing) region is "aws-cn". + Partition string + + // The service namespace that identifies the AWS product (for example, Amazon S3, IAM, or Amazon RDS). For a list of + // namespaces, see + // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces. + Service string + + // The region the resource resides in. Note that the ARNs for some resources do not require a region, so this + // component might be omitted. + Region string + + // The ID of the AWS account that owns the resource, without the hyphens. For example, 123456789012. Note that the + // ARNs for some resources don't require an account number, so this component might be omitted. + AccountID string + + // The content of this part of the ARN varies by service. It often includes an indicator of the type of resource — + // for example, an IAM user or Amazon RDS database - followed by a slash (/) or a colon (:), followed by the + // resource name itself. Some services allows paths for resource names, as described in + // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-paths. + Resource string +} + +// Parse parses an ARN into its constituent parts. +// +// Some example ARNs: +// arn:aws:elasticbeanstalk:us-east-1:123456789012:environment/My App/MyEnvironment +// arn:aws:iam::123456789012:user/David +// arn:aws:rds:eu-west-1:123456789012:db:mysql-db +// arn:aws:s3:::my_corporate_bucket/exampleobject.png +func Parse(arn string) (ARN, error) { + if !strings.HasPrefix(arn, arnPrefix) { + return ARN{}, errors.New(invalidPrefix) + } + sections := strings.SplitN(arn, arnDelimiter, arnSections) + if len(sections) != arnSections { + return ARN{}, errors.New(invalidSections) + } + return ARN{ + Partition: sections[sectionPartition], + Service: sections[sectionService], + Region: sections[sectionRegion], + AccountID: sections[sectionAccountID], + Resource: sections[sectionResource], + }, nil +} + +// String returns the canonical representation of the ARN +func (arn ARN) String() string { + return arnPrefix + + arn.Partition + arnDelimiter + + arn.Service + arnDelimiter + + arn.Region + arnDelimiter + + arn.AccountID + arnDelimiter + + arn.Resource +} diff --git a/aws/arn/arn_test.go b/aws/arn/arn_test.go new file mode 100644 index 00000000000..3dda7843aca --- /dev/null +++ b/aws/arn/arn_test.go @@ -0,0 +1,90 @@ +// +build go1.7 + +package arn + +import ( + "errors" + "testing" +) + +func TestParseARN(t *testing.T) { + cases := []struct { + input string + arn ARN + err error + }{ + { + input: "invalid", + err: errors.New(invalidPrefix), + }, + { + input: "arn:nope", + err: errors.New(invalidSections), + }, + { + input: "arn:aws:ecr:us-west-2:123456789012:repository/foo/bar", + arn: ARN{ + Partition: "aws", + Service: "ecr", + Region: "us-west-2", + AccountID: "123456789012", + Resource: "repository/foo/bar", + }, + }, + { + input: "arn:aws:elasticbeanstalk:us-east-1:123456789012:environment/My App/MyEnvironment", + arn: ARN{ + Partition: "aws", + Service: "elasticbeanstalk", + Region: "us-east-1", + AccountID: "123456789012", + Resource: "environment/My App/MyEnvironment", + }, + }, + { + input: "arn:aws:iam::123456789012:user/David", + arn: ARN{ + Partition: "aws", + Service: "iam", + Region: "", + AccountID: "123456789012", + Resource: "user/David", + }, + }, + { + input: "arn:aws:rds:eu-west-1:123456789012:db:mysql-db", + arn: ARN{ + Partition: "aws", + Service: "rds", + Region: "eu-west-1", + AccountID: "123456789012", + Resource: "db:mysql-db", + }, + }, + { + input: "arn:aws:s3:::my_corporate_bucket/exampleobject.png", + arn: ARN{ + Partition: "aws", + Service: "s3", + Region: "", + AccountID: "", + Resource: "my_corporate_bucket/exampleobject.png", + }, + }, + } + for _, tc := range cases { + t.Run(tc.input, func(t *testing.T) { + spec, err := Parse(tc.input) + if tc.arn != spec { + t.Errorf("Expected %q to parse as %v, but got %v", tc.input, tc.arn, spec) + } + if err == nil && tc.err != nil { + t.Errorf("Expected err to be %v, but got nil", tc.err) + } else if err != nil && tc.err == nil { + t.Errorf("Expected err to be nil, but got %v", err) + } else if err != nil && tc.err != nil && err.Error() != tc.err.Error() { + t.Errorf("Expected err to be %v, but got %v", tc.err, err) + } + }) + } +}