Merge lp:~julian-edwards/gwacl/container-acl into lp:gwacl

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: 139
Merged at revision: 132
Proposed branch: lp:~julian-edwards/gwacl/container-acl
Merge into: lp:gwacl
Prerequisite: lp:~julian-edwards/gwacl/extra-putpage-checks
Diff against target: 210 lines (+141/-1)
3 files modified
example/storage/run.go (+24/-1)
storage_base.go (+38/-0)
storage_base_test.go (+79/-0)
To merge this branch: bzr merge lp:~julian-edwards/gwacl/container-acl
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+171016@code.launchpad.net

Commit message

Add API call for SetContainerACL.

Description of the change

This adds the SetContainerACL call where you can set a container's ACL. This ACL has three options (unless you dive into shared access keys, which we don't do here):

 1. private
 2. container - allows full read-only access to all aspects of the container
 3. blob - allows people to retrieve known blobs from a container, nothing else including no container listing

I buggered up my new branch and branched off my previous one instead of trunk, so I stuck it as a prerequisite to make the diff correct. Ignore it here.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I see we have "*Request" and "*Params" types now. Doesn't it get a bit verbose, what with all the long camel-cased names?

.

In example/storage/run.go:

    case "containeracl":
        fmt.Printf("***:%s\n", acl)

—did you intend to remove this before submitting? Or if not, it could be clearer.

.

        if container == "" {
            return "", fmt.Errorf("Must supply container with containeracl")
        }

        if acl != "container" && acl != "blob" && acl != "private" {
            return "", fmt.Errorf("Usage: containeracl -container=<container> <container|blob|private>")
        }

—this would be so easy in Python: you'd just use the same single list of possibilities for the check and the error message. Go adds more overhead, pushing you towards a "switch" as the clearest expression.

But given that the "<container|blob|private>" choice keeps coming back, isn't it worth folding all this into a single data structure?

.

In SetContainerACL:

    switch req.Access {
    case "container", "blob":
        extraHeaders.Add("x-ms-blob-public-access", req.Access)
    case "private":
        // Don't add a header, Azure resets to private if it's omitted.
    default:
        panic("Access must be one of 'container', 'blob' or 'private'")
    }

Is the special treatment of "private" really pulling its weight here? If it's at all reasonable to extraHeaders.Add("x-ms-blob-public-access", "private") then I'd just do that unconditionally, and reduce this switch to just one line. It'd also save you some testing.

.

In TestInvalidAccessTypePanics, for testing the contents of the error, you probably want the ErrorMatches checker rather than Equals — since the panic is probably an error implementation, not a string as such. Assuming that it compares to a string may be a bit brittle. ErrorMatches takes a regex for the error text.

.

ISTR seeing some non-aligned initializer elements in there. Fine by me, but it may mean that you ought to re-run the formatter. Save somebody else the spurious diff.

.

And that's it! My compliments on the thorough testing.

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 24/06/13 20:05, Jeroen T. Vermeulen wrote:
> Review: Approve
>
> I see we have "*Request" and "*Params" types now. Doesn't it get a bit verbose, what with all the long camel-cased names?

Yeah, we need to make all of this consistent :/ I have a card on the
board already to review this before it gets out of hand.

>
> .
>
> In example/storage/run.go:
>
> case "containeracl":
> fmt.Printf("***:%s\n", acl)
>
> —did you intend to remove this before submitting? Or if not, it could be clearer.

I did mean to remove it, it was to debug the thing I complained about in
IRC yesterday where flags coming after positional args are treated like
positional args :/

>
> .
>
> if container == "" {
> return "", fmt.Errorf("Must supply container with containeracl")
> }
>
> if acl != "container" && acl != "blob" && acl != "private" {
> return "", fmt.Errorf("Usage: containeracl -container=<container> <container|blob|private>")
> }
>
> —this would be so easy in Python: you'd just use the same single list of possibilities for the check and the error message. Go adds more overhead, pushing you towards a "switch" as the clearest expression.
>
> But given that the "<container|blob|private>" choice keeps coming back, isn't it worth folding all this into a single data structure?

Maybe. My Go-fu is not strong enough to see this stuff as clearly and
intuitively as with Python.

We've tended to treat the example code as a bit of a hackfest but
perhaps it's time to clean it up and make it more of a good example than
a bad one.

>
> .
>
> In SetContainerACL:
>
> switch req.Access {
> case "container", "blob":
> extraHeaders.Add("x-ms-blob-public-access", req.Access)
> case "private":
> // Don't add a header, Azure resets to private if it's omitted.
> default:
> panic("Access must be one of 'container', 'blob' or 'private'")
> }
>
> Is the special treatment of "private" really pulling its weight here? If it's at all reasonable to extraHeaders.Add("x-ms-blob-public-access", "private") then I'd just do that unconditionally, and reduce this switch to just one line. It'd also save you some testing.

Yes. The header *has* to be omitted for private access. This does
require explicit testing. I added the empty case to make it obvious
what it's doing with that case and also so there's a sensible default case.

>
> .
>
> In TestInvalidAccessTypePanics, for testing the contents of the error, you probably want the ErrorMatches checker rather than Equals — since the panic is probably an error implementation, not a string as such. Assuming that it compares to a string may be a bit brittle. ErrorMatches takes a regex for the error text.

Good point, thanks.

>
> .
>
> ISTR seeing some non-aligned initializer elements in there. Fine by me, but it may mean that you ought to re-run the formatter. Save somebody else the spurious diff.

Yeah, will do.

>
> .
>
> And that's it! My compliments on the thorough testing.
>

Why thank you sir!

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Well here's why I used Equals instead of ErrorMatches:

storage_base_test.go:1177:
    context.SetContainerACL(&SetContainerACLRequest{
        Container: "mycontainer", Access: "thisisnotvalid"})
storage_base_test.go:1173:
    c.Assert(err, ErrorMatches, "Access must be one of 'container',
'blob' or 'private'")
... value string = "Access must be one of 'container', 'blob' or 'private'"
... regex string = "Access must be one of 'container', 'blob' or 'private'"
... Value is not an error

So recover() here returns a string not an error. Notice the declaration
of recover() in the Go source:

func recover() interface{}

The original panic panicked with a string, not an error.

138. By Julian Edwards

remove spurious printf

139. By Julian Edwards

format

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/storage/run.go'
2--- example/storage/run.go 2013-06-24 11:25:25 +0000
3+++ example/storage/run.go 2013-06-25 00:41:29 +0000
4@@ -26,6 +26,7 @@
5 var blobtype string
6 var size int
7 var pagerange string
8+var acl string
9
10 func checkContainerAndFilename() error {
11 if container == "" || filename == "" {
12@@ -34,7 +35,7 @@
13 return nil
14 }
15
16-var VALID_CMDS string = "<listcontainers|list|get|add|delete|putblob|putpage>"
17+var VALID_CMDS string = "<listcontainers|containeracl|list|get|add|delete|putblob|putpage>"
18
19 func badOperationError() error {
20 return fmt.Errorf("Must specify one of %s", VALID_CMDS)
21@@ -50,6 +51,7 @@
22 flag.StringVar(&blobtype, "blobtype", "", "Type of blob, 'page' or 'block'")
23 flag.IntVar(&size, "size", 0, "Size of blob to create for a page 'putblob'")
24 flag.StringVar(&pagerange, "pagerange", "", "When uploading to a page blob, this specifies what range in the blob. Use the format 'start-end', e.g. -pagerange 1024-2048")
25+ flag.StringVar(&acl, "acl", "", "When using 'containeracl', specify an ACL type")
26 flag.Parse()
27
28 if account == "" {
29@@ -63,6 +65,14 @@
30 switch flag.Arg(0) {
31 case "listcontainers":
32 return "listcontainers", nil
33+ case "containeracl":
34+ if container == "" {
35+ return "", fmt.Errorf("Must supply container with containeracl")
36+ }
37+ if acl != "container" && acl != "blob" && acl != "private" {
38+ return "", fmt.Errorf("Usage: containeracl -container=<container> <container|blob|private>")
39+ }
40+ return "containeracl", nil
41 case "list":
42 if container == "" {
43 return "", fmt.Errorf("Must supply container with 'list'")
44@@ -119,6 +129,9 @@
45 List files in a container:
46 list -container=<container>
47
48+ Set access on a container:
49+ containeracl -container=<container> -acl <container|blob|private>
50+
51 Get a file from a container (it's returned on stdout):
52 get -container=<container> -filename=<filename>
53
54@@ -158,6 +171,14 @@
55 }
56 }
57
58+func containeracl(storage gwacl.StorageContext) {
59+ err := storage.SetContainerACL(&gwacl.SetContainerACLRequest{
60+ Container: container,
61+ Access: acl,
62+ })
63+ dumpError(err)
64+}
65+
66 func list(storage gwacl.StorageContext) {
67 request := &gwacl.ListBlobsRequest{
68 Container: container, Prefix: prefix}
69@@ -260,6 +281,8 @@
70 switch op {
71 case "listcontainers":
72 listcontainers(storage)
73+ case "containeracl":
74+ containeracl(storage)
75 case "list":
76 list(storage)
77 case "add":
78
79=== modified file 'storage_base.go'
80--- storage_base.go 2013-06-25 00:31:29 +0000
81+++ storage_base.go 2013-06-25 00:41:29 +0000
82@@ -615,3 +615,41 @@
83 }
84 return response.Body, nil
85 }
86+
87+type SetContainerACLRequest struct {
88+ Container string // Container name in the storage account
89+ Access string // "container", "blob", or "private"
90+}
91+
92+// SetContainerACL sets the specified container's access rights.
93+// See http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx
94+func (context *StorageContext) SetContainerACL(req *SetContainerACLRequest) error {
95+ uri := addURLQueryParams(
96+ context.getContainerURL(req.Container),
97+ "restype", "container",
98+ "comp", "acl")
99+
100+ extraHeaders := http.Header{}
101+ switch req.Access {
102+ case "container", "blob":
103+ extraHeaders.Add("x-ms-blob-public-access", req.Access)
104+ case "private":
105+ // Don't add a header, Azure resets to private if it's omitted.
106+ default:
107+ panic("Access must be one of 'container', 'blob' or 'private'")
108+ }
109+
110+ _, err := context.performRequest(requestParams{
111+ Method: "PUT",
112+ URL: uri,
113+ APIVersion: "2009-09-19",
114+ ExtraHeaders: extraHeaders,
115+ ExpectedStatus: http.StatusOK,
116+ })
117+
118+ if err != nil {
119+ msg := fmt.Sprintf("failed to set ACL for container %s: ", req.Container)
120+ return extendError(err, msg)
121+ }
122+ return nil
123+}
124
125=== modified file 'storage_base_test.go'
126--- storage_base_test.go 2013-06-25 00:31:29 +0000
127+++ storage_base_test.go 2013-06-25 00:41:29 +0000
128@@ -1132,3 +1132,82 @@
129 c.Check(err, ErrorMatches, ".*Frotzed.*")
130 c.Check(err, ErrorMatches, ".*failed to get blob.*")
131 }
132+
133+type TestSetContainerACL struct{}
134+
135+var _ = Suite(&TestSetContainerACL{})
136+
137+func (suite *TestSetContainerACL) TestHappyPath(c *C) {
138+ response := makeHttpResponse(http.StatusOK, "")
139+ transport := &TestTransport{Response: response}
140+ context := makeStorageContext(transport)
141+ err := context.SetContainerACL(&SetContainerACLRequest{
142+ Container: "mycontainer", Access: "container"})
143+
144+ c.Assert(err, IsNil)
145+ c.Check(transport.Request.Method, Equals, "PUT")
146+ c.Check(transport.Request.URL.String(), Matches,
147+ fmt.Sprintf(
148+ "http://%s.blob.core.windows.net/mycontainer?.*", context.Account))
149+ c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
150+ "comp": {"acl"},
151+ "restype": {"container"},
152+ })
153+
154+ c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
155+ c.Check(transport.Request.Header.Get("x-ms-blob-public-access"), Equals, "container")
156+}
157+
158+func (suite *TestSetContainerACL) TestAcceptsBlobAccess(c *C) {
159+ response := makeHttpResponse(http.StatusOK, "")
160+ transport := &TestTransport{Response: response}
161+ context := makeStorageContext(transport)
162+ err := context.SetContainerACL(&SetContainerACLRequest{
163+ Container: "mycontainer", Access: "blob"})
164+ c.Assert(err, IsNil)
165+ c.Check(transport.Request.Header.Get("x-ms-blob-public-access"), Equals, "blob")
166+}
167+
168+func (suite *TestSetContainerACL) TestAccessHeaderOmittedWhenPrivate(c *C) {
169+ response := makeHttpResponse(http.StatusOK, "")
170+ transport := &TestTransport{Response: response}
171+ context := makeStorageContext(transport)
172+ err := context.SetContainerACL(&SetContainerACLRequest{
173+ Container: "mycontainer", Access: "private"})
174+
175+ c.Assert(err, IsNil)
176+ c.Check(transport.Request.Header.Get("x-ms-blob-public-access"), Equals, "")
177+}
178+
179+func (suite *TestSetContainerACL) TestInvalidAccessTypePanics(c *C) {
180+ defer func() {
181+ err := recover()
182+ c.Assert(err, Equals, "Access must be one of 'container', 'blob' or 'private'")
183+ }()
184+ context := makeStorageContext(&TestTransport{})
185+ context.SetContainerACL(&SetContainerACLRequest{
186+ Container: "mycontainer", Access: "thisisnotvalid"})
187+ c.Assert("This test failed", Equals, "because there was no panic")
188+}
189+
190+func (suite *TestSetContainerACL) TestClientSideError(c *C) {
191+ error := fmt.Errorf("canned-error")
192+ context := makeStorageContext(&TestTransport{Error: error})
193+ err := context.SetContainerACL(&SetContainerACLRequest{
194+ Container: "mycontainer", Access: "private"})
195+ c.Assert(err, NotNil)
196+}
197+
198+// Azure HTTP errors (for instance 404 responses) are propagated back to
199+// the caller as ServerError objects.
200+func (suite *TestSetContainerACL) TestServerError(c *C) {
201+ response := makeHttpResponse(http.StatusNotFound, "not found")
202+ context := makeStorageContext(&TestTransport{Response: response})
203+ err := context.SetContainerACL(&SetContainerACLRequest{
204+ Container: "mycontainer", Access: "private"})
205+ c.Assert(err, NotNil)
206+ serverError, ok := err.(*ServerError)
207+ c.Check(ok, Equals, true)
208+ c.Check(serverError.HTTPStatus.StatusCode(), Equals, http.StatusNotFound)
209+ c.Check(IsNotFoundError(err), Equals, true)
210+}

Subscribers

People subscribed via source and target branches

to all changes: