Merge lp:~fwereade/juju-core/provider-skeleton into lp:~go-bot/juju-core/trunk

Proposed by William Reade
Status: Work in progress
Proposed branch: lp:~fwereade/juju-core/provider-skeleton
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1019 lines (+944/-4)
13 files modified
doc/hacking-providers.txt (+129/-0)
provider/joyent/environ.go (+0/-4)
provider/skeleton/config.go (+134/-0)
provider/skeleton/config_test.go (+203/-0)
provider/skeleton/environ.go (+119/-0)
provider/skeleton/environ_firewall.go (+29/-0)
provider/skeleton/environ_instance.go (+43/-0)
provider/skeleton/export_test.go (+10/-0)
provider/skeleton/instance.go (+50/-0)
provider/skeleton/instance_firewall.go (+26/-0)
provider/skeleton/provider.go (+121/-0)
provider/skeleton/skeleton_test.go (+27/-0)
provider/skeleton/storage.go (+53/-0)
To merge this branch: bzr merge lp:~fwereade/juju-core/provider-skeleton
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+189638@code.launchpad.net

Description of the change

skeleton provider

...and rudimentary initial implementation notes.

https://codereview.appspot.com/14494043/

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Reviewers: mp+189638_code.launchpad.net,

Message:
Please take a look.

Description:
skeleton provider

...and rudimentary initial implementation notes.

https://code.launchpad.net/~fwereade/juju-core/provider-skeleton/+merge/189638

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/14494043/

Affected files (+926, -0 lines):
   A [revision details]
   A doc/hacking-providers.txt
   A provider/skeleton/config.go
   A provider/skeleton/config_test.go
   A provider/skeleton/environ.go
   A provider/skeleton/environ_firewall.go
   A provider/skeleton/environ_instance.go
   A provider/skeleton/export_test.go
   A provider/skeleton/instance.go
   A provider/skeleton/instance_firewall.go
   A provider/skeleton/provider.go
   A provider/skeleton/skeleton_test.go
   A provider/skeleton/storage.go

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (4.1 KiB)

This is great, thanks.
LGTM modulo the below comments and suggestions.

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt
File doc/hacking-providers.txt (right):

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt#newcode9
doc/hacking-providers.txt:9: "provider" to juju; this basically involves
defining how the substrate can be
s/basically//

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt#newcode28
doc/hacking-providers.txt:28: ...which roughly correspond to the
environs.InstanceBroker interface.
I wonder if it might be nice to add some godoc links,
although it wouldn't improve text-only readability.

For example
http://godoc.org/launchpad.net/juju-core/environs#InstanceBroker

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt#newcode84
doc/hacking-providers.txt:84: support library be exposed as interfaces,
to enable easy mocking and hence allow
I'm not sure that exposing the support library as an interface
necessarily makes it much easier to mock. The interfaces can usually
be defined in the calling package.

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt#newcode103
doc/hacking-providers.txt:103: So we have "dependencies.tsv" in the root
of juju-core. Tooling support is spotty
Perhaps you could mention launchpad.net/godeps here - it is used to
generate the file and can be used to update the dependencies too.

https://codereview.appspot.com/14494043/diff/1/doc/hacking-providers.txt#newcode113
doc/hacking-providers.txt:113: and replace every instance of the
characters "skeleton" with your new provider name.
s/characters/word/ ?

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go
File provider/skeleton/config.go (right):

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode16
provider/skeleton/config.go:16: const boilerplateConfig = `skeleton:
All the other providers put this on a new line and use [1:] at the end.
We should keep them consistent.

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode19
provider/skeleton/config.go:19: # this exists to demonstrate how to deal
with sensitive values.
Let's use full sentences here, to fit with the changes made in
https://codereview.appspot.com/14426046/

and four-space indents likewise.

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode33
provider/skeleton/config.go:33: var configFields = schema.Fields{
Please let's have doc comments on all variable and function
definitions, as it's important that people understand the
purpose of each part of the skeleton they're supposed to
be adapting.

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode75
provider/skeleton/config.go:75: // present in validated, and defaults
will be inserted if necessary. If the
s/validated/newAttrs/ ?

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode77
provider/skeleton/config.go:77: // whatever checks you need here, before
continuing.
s/,//

https://codereview.appspot.com/14494043/diff/1/provider/skeleton/config.go#newcode78
provider...

Read more...

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

it would be great if this could resurrected and pushed for people starting in on new providers.

Revision history for this message
John A Meinel (jameinel) wrote :

I'm pretty sure we wanted to land this, is there anything we can do to help it along?

1901. By William Reade

merge parent

1902. By William Reade

now satisifes Environ interface again

Unmerged revisions

1902. By William Reade

now satisifes Environ interface again

1901. By William Reade

merge parent

1900. By William Reade

drop overly-rough doc bits

1899. By William Reade

add comments

1898. By William Reade

added sample config tests

1897. By William Reade

merge parent

1896. By William Reade

finish Prepare logic

1895. By William Reade

merge parent

1894. By William Reade

basic skeleton provider, docs in progress

1893. By William Reade

merge parent

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'doc/hacking-providers.txt'
2--- doc/hacking-providers.txt 1970-01-01 00:00:00 +0000
3+++ doc/hacking-providers.txt 2014-04-18 14:48:42 +0000
4@@ -0,0 +1,129 @@
5+
6+
7+Juju providers
8+--------------
9+
10+Juju currently allows you to deploy and scale services on deveral different
11+kinds of substrate, including MAAS, Azure, AWS, and a variety of Openstack clouds
12+(including HPCloud). To add a new kind of substrate, you need to add a new
13+"provider" to juju; this basically involves defining how the substrate can be
14+configured, and writing the code that supports the (relatively few) provider
15+operations that juju needs to know.
16+
17+
18+Required operations
19+-------------------
20+
21+Juju requires that a cloud provider support the following operations:
22+
23+ * start a fresh instance (Environ.StartInstance)
24+ * running a particular series of Ubuntu
25+ * with foreknowledge of what processor architecture it will run
26+ * with a customized cloud-init script
27+ * ...and report the new instance's hardware characteristics
28+ * query the list of running instances (Environ.Instances, Environ.AllInstances)
29+ * stop a running instance (Environ.StopInstances)
30+ * discover a running instances's addresses (Instance.Addresses)
31+
32+...which roughly correspond to the environs.InstanceBroker interface.
33+
34+To enable the benefits of containerization for your cloud provider, you will need
35+some way to create new internal addresses and assign them to instances. The actual
36+interface for this is still under discussion.
37+
38+Juju strongly recommends that a cloud provider support the following operations:
39+
40+ * read names in object storage (StorageReader.List)
41+ * read a named file from object storage (StorageReader.Get)
42+ * make a named file from object storage available over HTTP (StorageReader.URL)
43+ * write a named file to object storage (StorageWriter.Put)
44+ * remove a named file from object storage (StorageWriter.Remove)
45+
46+...but if you can't do storage, come and talk to us: there are potential ways
47+round this, and by the time you read this they may be a reality already. But it
48+would not be wise to implement in-environment storage without discussion.
49+
50+Finally, if the cloud provider supports them, juju appreciates support for the
51+following groups of operations:
52+
53+ * open a port for every machine in an environment (Environ.OpenPorts)
54+ * list open ports for every machine in an environment (Environ.Ports)
55+ * close a port for every machine in an environment (Environ.ClosePorts)
56+
57+ * open a port for one machine in an environment (Instance.OpenPorts)
58+ * list open ports for one machine in an environment (Instance.Ports)
59+ * close a port for one machine in an environment (Instance.ClosePorts)
60+
61+...but if you can't support those, don't worry: just make the methods return without
62+error and juju will make do.
63+
64+If your cloud doesn't assign a public address for every new instance, you will
65+almost certainly need to do so for the bootstrap node (so that the client can
66+communicate with the API); and will most likely need to implement any OpenPorts
67+methods such that they ensure the target instance(s) have public addresses
68+assigned.
69+
70+
71+Implementing the required operations
72+------------------------------------
73+
74+We very strongly recommend that you implement a library layer exposing the above
75+before attempting to write a provider for juju. Mixing the low-level concerns of
76+talking to your API into the provider -- which is essentially an adapter layer --
77+is a recipe for trouble. The following packages may serve as useful inspiration;
78+all were written for, and are used by, juju providers.
79+
80+ * launchpad.net/goamz (ec2 provider)
81+ * launchpad.net/gwacl (azure provider)
82+ * launchpad.net/gomaasapi (maas provider)
83+ * launchpad.net/goose (openstack provider)
84+
85+We have in the past tried to write large-scale test doubles against which common
86+provider tests can run within juju, but have found in practice that this approach
87+is often difficult to work with effectively. We therefore now suggest that your
88+support library be exposed as interfaces, to enable easy mocking and hence allow
89+for comprehensive unit testing of the provider package.
90+
91+We have a separate team working on cross-provider functional tests for juju. To
92+arrange automatic testing against your cloud, please get in touch.
93+
94+
95+Dependency management
96+---------------------
97+
98+Dependency management can be problematic in go. Despite the advice above, library
99+development tends to proceed in parallel with provider development; and when this
100+is happening, it's really unpleasant having to manage compatibility. The glib
101+answer is to stipulate that you'll only make backward-compatible changes to your
102+support library, and will publish a new version at a new URL whenever you make a
103+breaking change; but in our experience this does not work well in practice. Even
104+when the same party controls both sides, it's all too easy to break in surprising
105+ways.
106+
107+So we have "dependencies.tsv" in the root of juju-core. Tooling support is spotty
108+but improving, and if you keep it up to date you should be free at least to
109+change your support library without breaking juju-core. Other clients are, sad
110+to say, on their own.
111+
112+
113+Implementing an actual provider
114+-------------------------------
115+
116+Start by copying the "providers/skeleton" package into a new subpackage of "providers",
117+and replace every instance of the characters "skeleton" with your new provider name.
118+
119+Then take a look at "config.go", and change or remove the example fields to suit your
120+new provider. There will probably be at least one field holding credentials, that
121+should replace "skeleton-secret-field"; and there may be other fields that should be
122+handled similarly to "skeleton-default-field" and/or "skeleton-immutable-field"; for
123+example, a "region" setting should probably have a default value and should almost
124+certainly not be mutable for a given environment.
125+
126+Don't forget to keep the config tests comprehensive and up to date.
127+
128+Once you've got a working config, you should be in a position to start trying to
129+bootstrap the environment, and implementing methods as they fail. To begin with,
130+you'll definitely need to implement (much of) the Storage type; then you'll need
131+Environ.StartInstance, and Environ.Instances; and then the Instance type.
132+
133+To Be Continued...
134
135=== modified file 'provider/joyent/environ.go'
136--- provider/joyent/environ.go 2014-04-09 16:36:12 +0000
137+++ provider/joyent/environ.go 2014-04-18 14:48:42 +0000
138@@ -128,10 +128,6 @@
139 return env.getSnapshot().storage
140 }
141
142-func (env *joyentEnviron) PublicStorage() storage.StorageReader {
143- return environs.EmptyStorage
144-}
145-
146 func (env *joyentEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
147 return common.Bootstrap(ctx, env, cons)
148 }
149
150=== added directory 'provider/skeleton'
151=== added file 'provider/skeleton/config.go'
152--- provider/skeleton/config.go 1970-01-01 00:00:00 +0000
153+++ provider/skeleton/config.go 2014-04-18 14:48:42 +0000
154@@ -0,0 +1,134 @@
155+// Copyright 2013 Canonical Ltd.
156+// Licensed under the AGPLv3, see LICENCE file for details.
157+
158+package skeleton
159+
160+import (
161+ "fmt"
162+
163+ "launchpad.net/juju-core/environs/config"
164+ "launchpad.net/juju-core/schema"
165+ "launchpad.net/juju-core/utils"
166+)
167+
168+// boilerplateConfig will be shown in help output, so please keep it up to
169+// date when you change environment configuration below.
170+const boilerplateConfig = `skeleton:
171+ type: skeleton
172+
173+ # this exists to demonstrate how to deal with sensitive values.
174+ skeleton-secret-field: <cloud credentials, for example>
175+
176+ # this exists to demonstrate how to deal with values that can't change; and
177+ # also how to use Prepare to fill in values that don't makes sense with
178+ # static defaults but can be inferred or chosen at runtime.
179+ # skeleton-immutable-field: <a storage bucket name, for example>
180+
181+ # this exists to demonstrate how to deal with static default values that
182+ # some users may wish to override
183+ # skeleton-default-field: <specific default value>
184+
185+`
186+
187+var configFields = schema.Fields{
188+ "skeleton-secret-field": schema.String(),
189+ "skeleton-immutable-field": schema.String(),
190+ "skeleton-default-field": schema.String(),
191+}
192+
193+var configDefaultFields = schema.Defaults{
194+ "skeleton-default-field": "<specific default value>",
195+}
196+
197+var configSecretFields = []string{
198+ "skeleton-secret-field",
199+}
200+
201+var configImmutableFields = []string{
202+ "skeleton-immutable-field",
203+}
204+
205+func prepareConfig(cfg *config.Config) (*config.Config, error) {
206+ // Turn an incomplete config into a valid one, if possible.
207+ attrs := cfg.UnknownAttrs()
208+ if _, ok := attrs["skeleton-immutable-field"]; !ok {
209+ uuid, err := utils.NewUUID()
210+ if err != nil {
211+ return nil, fmt.Errorf("cannot generate skeleton-immutable-field")
212+ }
213+ attrs["skeleton-immutable-field"] = fmt.Sprintf("%x", uuid.Raw())
214+ }
215+ return cfg.Apply(attrs)
216+}
217+
218+func validateConfig(cfg *config.Config, old *environConfig) (*environConfig, error) {
219+ // Check sanity of juju-level fields.
220+ var oldCfg *config.Config
221+ if old != nil {
222+ oldCfg = old.Config
223+ }
224+ if err := config.Validate(cfg, oldCfg); err != nil {
225+ return nil, err
226+ }
227+
228+ // Extract validated provider-specific fields. All of configFields will be
229+ // present in validated, and defaults will be inserted if necessary. If the
230+ // schema you passed in doesn't quite express what you need, you can make
231+ // whatever checks you need here, before continuing.
232+ // In particular, if you want to extract (say) credentials from the user's
233+ // shell environment variables, you'll need to allow missing values to pass
234+ // through the schema by setting a value of schema.Omit in the configFields
235+ // map, and then to set and check them at this point. These values *must* be
236+ // stored in newAttrs: a Config will be generated on the user's machine only
237+ // to begin with, and will subsequently be used on a different machine that
238+ // will probably not have those variables set.
239+ newAttrs, err := cfg.ValidateUnknownAttrs(configFields, configDefaultFields)
240+ if err != nil {
241+ return nil, err
242+ }
243+ for field := range configFields {
244+ if newAttrs[field] == "" {
245+ return nil, fmt.Errorf("%s: must not be empty", field)
246+ }
247+ }
248+
249+ // If an old config was supplied, check any immutable fields have not changed.
250+ if old != nil {
251+ for _, field := range configImmutableFields {
252+ if old.attrs[field] != newAttrs[field] {
253+ return nil, fmt.Errorf(
254+ "%s: cannot change from %v to %v",
255+ field, old.attrs[field], newAttrs[field],
256+ )
257+ }
258+ }
259+ }
260+
261+ // Merge the validated provider-specific fields into the original config,
262+ // to ensure the object we return is internally consistent.
263+ newCfg, err := cfg.Apply(newAttrs)
264+ if err != nil {
265+ return nil, err
266+ }
267+ return &environConfig{
268+ Config: newCfg,
269+ attrs: newAttrs,
270+ }, nil
271+}
272+
273+type environConfig struct {
274+ *config.Config
275+ attrs map[string]interface{}
276+}
277+
278+func (ecfg *environConfig) secretField() string {
279+ return ecfg.attrs["skeleton-secret-field"].(string)
280+}
281+
282+func (ecfg *environConfig) immutableField() string {
283+ return ecfg.attrs["skeleton-immutable-field"].(string)
284+}
285+
286+func (ecfg *environConfig) defaultField() string {
287+ return ecfg.attrs["skeleton-default-field"].(string)
288+}
289
290=== added file 'provider/skeleton/config_test.go'
291--- provider/skeleton/config_test.go 1970-01-01 00:00:00 +0000
292+++ provider/skeleton/config_test.go 2014-04-18 14:48:42 +0000
293@@ -0,0 +1,203 @@
294+// Copyright 2013 Canonical Ltd.
295+// Licensed under the AGPLv3, see LICENCE file for details.
296+
297+package skeleton_test
298+
299+import (
300+ gc "launchpad.net/gocheck"
301+
302+ "launchpad.net/juju-core/environs"
303+ "launchpad.net/juju-core/environs/config"
304+ "launchpad.net/juju-core/provider/skeleton"
305+ "launchpad.net/juju-core/testing"
306+ "launchpad.net/juju-core/testing/testbase"
307+)
308+
309+func newConfig(c *gc.C, attrs testing.Attrs) *config.Config {
310+ attrs = testing.FakeConfig().Merge(attrs)
311+ cfg, err := config.New(config.NoDefaults, attrs)
312+ c.Assert(err, gc.IsNil)
313+ return cfg
314+}
315+
316+func validAttrs() testing.Attrs {
317+ return testing.FakeConfig().Merge(testing.Attrs{
318+ "type": "skeleton",
319+ "skeleton-secret-field": "seekrit",
320+ "skeleton-immutable-field": "static",
321+ })
322+}
323+
324+type ConfigSuite struct {
325+ testbase.LoggingSuite
326+}
327+
328+var _ = gc.Suite(&ConfigSuite{})
329+
330+var newConfigTests = []struct {
331+ info string
332+ insert testing.Attrs
333+ remove []string
334+ expect testing.Attrs
335+ err string
336+}{{
337+ info: "skeleton-immutable-field is required",
338+ remove: []string{"skeleton-immutable-field"},
339+ err: "skeleton-immutable-field: expected string, got nothing",
340+}, {
341+ info: "skeleton-immutable-field cannot be empty",
342+ insert: testing.Attrs{"skeleton-immutable-field": ""},
343+ err: "skeleton-immutable-field: must not be empty",
344+}, {
345+ info: "skeleton-secret-field is required",
346+ remove: []string{"skeleton-secret-field"},
347+ err: "skeleton-secret-field: expected string, got nothing",
348+}, {
349+ info: "skeleton-secret-field cannot be empty",
350+ insert: testing.Attrs{"skeleton-secret-field": ""},
351+ err: "skeleton-secret-field: must not be empty",
352+}, {
353+ info: "skeleton-default-field is inserted if missing",
354+ expect: testing.Attrs{"skeleton-default-field": "<specific default value>"},
355+}, {
356+ info: "skeleton-default-field cannot be empty",
357+ insert: testing.Attrs{"skeleton-default-field": ""},
358+ err: "skeleton-default-field: must not be empty",
359+}, {
360+ info: "skeleton-default-field is untouched if present",
361+ insert: testing.Attrs{"skeleton-default-field": "<user value>"},
362+ expect: testing.Attrs{"skeleton-default-field": "<user value>"},
363+}, {
364+ info: "unknown field is not touched",
365+ insert: testing.Attrs{"unknown-field": 12345},
366+ expect: testing.Attrs{"unknown-field": 12345},
367+}}
368+
369+func (*ConfigSuite) TestNewEnvironConfig(c *gc.C) {
370+ for i, test := range newConfigTests {
371+ c.Logf("test %d: %s", i, test.info)
372+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
373+ testConfig := newConfig(c, attrs)
374+ environ, err := environs.New(testConfig)
375+ if test.err == "" {
376+ c.Check(err, gc.IsNil)
377+ attrs := environ.Config().AllAttrs()
378+ for field, value := range test.expect {
379+ c.Check(attrs[field], gc.Equals, value)
380+ }
381+ } else {
382+ c.Check(environ, gc.IsNil)
383+ c.Check(err, gc.ErrorMatches, test.err)
384+ }
385+ }
386+}
387+
388+func (*ConfigSuite) TestValidateNewConfig(c *gc.C) {
389+ for i, test := range newConfigTests {
390+ c.Logf("test %d: %s", i, test.info)
391+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
392+ testConfig := newConfig(c, attrs)
393+ validatedConfig, err := skeleton.Provider.Validate(testConfig, nil)
394+ if test.err == "" {
395+ c.Check(err, gc.IsNil)
396+ attrs := validatedConfig.AllAttrs()
397+ for field, value := range test.expect {
398+ c.Check(attrs[field], gc.Equals, value)
399+ }
400+ } else {
401+ c.Check(validatedConfig, gc.IsNil)
402+ c.Check(err, gc.ErrorMatches, "invalid config: "+test.err)
403+ }
404+ }
405+}
406+
407+func (*ConfigSuite) TestValidateOldConfig(c *gc.C) {
408+ knownGoodConfig := newConfig(c, validAttrs())
409+ for i, test := range newConfigTests {
410+ c.Logf("test %d: %s", i, test.info)
411+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
412+ testConfig := newConfig(c, attrs)
413+ validatedConfig, err := skeleton.Provider.Validate(knownGoodConfig, testConfig)
414+ if test.err == "" {
415+ c.Check(err, gc.IsNil)
416+ attrs := validatedConfig.AllAttrs()
417+ for field, value := range validAttrs() {
418+ c.Check(attrs[field], gc.Equals, value)
419+ }
420+ } else {
421+ c.Check(validatedConfig, gc.IsNil)
422+ c.Check(err, gc.ErrorMatches, "invalid base config: "+test.err)
423+ }
424+ }
425+}
426+
427+var changeConfigTests = []struct {
428+ info string
429+ insert testing.Attrs
430+ remove []string
431+ expect testing.Attrs
432+ err string
433+}{{
434+ info: "no change, no error",
435+ expect: validAttrs(),
436+}, {
437+ info: "can change skeleton-secret-field",
438+ insert: testing.Attrs{"skeleton-secret-field": "okkult"},
439+ expect: testing.Attrs{"skeleton-secret-field": "okkult"},
440+}, {
441+ info: "can change skeleton-default-field",
442+ insert: testing.Attrs{"skeleton-default-field": "different"},
443+ expect: testing.Attrs{"skeleton-default-field": "different"},
444+}, {
445+ info: "cannot change skeleton-immutable-field",
446+ insert: testing.Attrs{"skeleton-immutable-field": "mutant"},
447+ err: "skeleton-immutable-field: cannot change from static to mutant",
448+}, {
449+ info: "can insert unknown field",
450+ insert: testing.Attrs{"unknown": "ignoti"},
451+ expect: testing.Attrs{"unknown": "ignoti"},
452+}}
453+
454+func (s *ConfigSuite) TestValidateChange(c *gc.C) {
455+ baseConfig := newConfig(c, validAttrs())
456+ for i, test := range changeConfigTests {
457+ c.Logf("test %d: %s", i, test.info)
458+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
459+ testConfig := newConfig(c, attrs)
460+ validatedConfig, err := skeleton.Provider.Validate(testConfig, baseConfig)
461+ if test.err == "" {
462+ c.Check(err, gc.IsNil)
463+ attrs := validatedConfig.AllAttrs()
464+ for field, value := range test.expect {
465+ c.Check(attrs[field], gc.Equals, value)
466+ }
467+ } else {
468+ c.Check(validatedConfig, gc.IsNil)
469+ c.Check(err, gc.ErrorMatches, "invalid config change: "+test.err)
470+ }
471+ }
472+}
473+
474+func (s *ConfigSuite) TestSetConfig(c *gc.C) {
475+ baseConfig := newConfig(c, validAttrs())
476+ for i, test := range changeConfigTests {
477+ c.Logf("test %d: %s", i, test.info)
478+ environ, err := environs.New(baseConfig)
479+ c.Assert(err, gc.IsNil)
480+ attrs := validAttrs().Merge(test.insert).Delete(test.remove...)
481+ testConfig := newConfig(c, attrs)
482+ err = environ.SetConfig(testConfig)
483+ newAttrs := environ.Config().AllAttrs()
484+ if test.err == "" {
485+ c.Check(err, gc.IsNil)
486+ for field, value := range test.expect {
487+ c.Check(newAttrs[field], gc.Equals, value)
488+ }
489+ } else {
490+ c.Check(err, gc.ErrorMatches, test.err)
491+ for field, value := range baseConfig.UnknownAttrs() {
492+ c.Check(newAttrs[field], gc.Equals, value)
493+ }
494+ }
495+ }
496+}
497
498=== added file 'provider/skeleton/environ.go'
499--- provider/skeleton/environ.go 1970-01-01 00:00:00 +0000
500+++ provider/skeleton/environ.go 2014-04-18 14:48:42 +0000
501@@ -0,0 +1,119 @@
502+// Copyright 2013 Canonical Ltd.
503+// Licensed under the AGPLv3, see LICENCE file for details.
504+
505+package skeleton
506+
507+import (
508+ "sync"
509+
510+ "launchpad.net/juju-core/constraints"
511+ "launchpad.net/juju-core/environs"
512+ "launchpad.net/juju-core/environs/config"
513+ "launchpad.net/juju-core/environs/storage"
514+ "launchpad.net/juju-core/juju/arch"
515+ "launchpad.net/juju-core/provider/common"
516+ "launchpad.net/juju-core/state"
517+ "launchpad.net/juju-core/state/api"
518+)
519+
520+// This file contains the core of the skeleton Environ implementation. You will
521+// probably not need to change this file very much to begin with; and if you
522+// never need to add any more fields, you may never need to touch it.
523+//
524+// The rest of the implementation is split into environ_instance.go (which
525+// must be implemented ) and environ_firewall.go (which can be safely
526+// ignored until you've got an environment bootstrapping successfully).
527+
528+type environ struct {
529+ // This is used to check sanity of provisioning requests ahead of time. The
530+ // default implementation doesn't check anything; an ideal environ will use
531+ // its own PrecheckInstance method to prevent impossible provisioning
532+ // requests before they're made.
533+ common.NopPrecheckerPolicy
534+
535+ // The SupportsUnitPlacementPolicy makes unit placement available on the
536+ // provider. The only reason to replace it would be if you were implementing
537+ // a provider like azure, in which we had to sacrifice unit placement in
538+ // favour of making it possible to keep services highly available.
539+ common.SupportsUnitPlacementPolicy
540+
541+ name string
542+ // All mutating operations should lock the mutex. Non-mutating operations
543+ // should read all fields (other than name, which is immutable) from a
544+ // shallow copy taken with getSnapshot().
545+ // This advice is predicated on the goroutine-safety of the values of the
546+ // affected fields.
547+ lock sync.Mutex
548+ ecfg *environConfig
549+ storage storage.Storage
550+}
551+
552+var _ environs.Environ = (*environ)(nil)
553+
554+func (env *environ) Name() string {
555+ return env.name
556+}
557+
558+func (*environ) Provider() environs.EnvironProvider {
559+ return providerInstance
560+}
561+
562+func (env *environ) SetConfig(cfg *config.Config) error {
563+ env.lock.Lock()
564+ defer env.lock.Unlock()
565+ ecfg, err := validateConfig(cfg, env.ecfg)
566+ if err != nil {
567+ return err
568+ }
569+ storage, err := newStorage(ecfg)
570+ if err != nil {
571+ return err
572+ }
573+ env.ecfg = ecfg
574+ env.storage = storage
575+ return nil
576+}
577+
578+func (env *environ) getSnapshot() *environ {
579+ env.lock.Lock()
580+ clone := *env
581+ env.lock.Unlock()
582+ clone.lock = sync.Mutex{}
583+ return &clone
584+}
585+
586+func (env *environ) Config() *config.Config {
587+ return env.getSnapshot().ecfg.Config
588+}
589+
590+func (env *environ) Storage() storage.Storage {
591+ return env.getSnapshot().storage
592+}
593+
594+func (env *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
595+ // You can probably ignore this method; the common implementation should work.
596+ return common.Bootstrap(ctx, env, cons)
597+}
598+
599+func (env *environ) StateInfo() (*state.Info, *api.Info, error) {
600+ // You can probably ignore this method; the common implementation should work.
601+ return common.StateInfo(env)
602+}
603+
604+func (env *environ) Destroy() error {
605+ // You can probably ignore this method; the common implementation should work.
606+ return common.Destroy(env)
607+}
608+
609+// SupportedArchitectures is specified on the EnvironCapability interface.
610+func (env *environ) SupportedArchitectures() ([]string, error) {
611+ // An ideal implementation will inspect the tools, images, and instance types
612+ // available in the environment to return correct values here.
613+ return arch.AllSupportedArches, nil
614+}
615+
616+// SupportNetworks is specified on the EnvironCapability interface.
617+func (env *environ) SupportNetworks() bool {
618+ // An ideal implementation will support networking.
619+ return false
620+}
621
622=== added file 'provider/skeleton/environ_firewall.go'
623--- provider/skeleton/environ_firewall.go 1970-01-01 00:00:00 +0000
624+++ provider/skeleton/environ_firewall.go 2014-04-18 14:48:42 +0000
625@@ -0,0 +1,29 @@
626+// Copyright 2013 Canonical Ltd.
627+// Licensed under the AGPLv3, see LICENCE file for details.
628+
629+package skeleton
630+
631+import (
632+ "launchpad.net/juju-core/instance"
633+)
634+
635+// Implementing the methods below (to do something other than return nil) will
636+// cause `juju expose` to work when the firewall-mode is "global". If you
637+// implement one of them, you should implement them all.
638+
639+func (env *environ) OpenPorts(ports []instance.Port) error {
640+ logger.Warningf("pretending to open ports %v for all instances", ports)
641+ _ = env.getSnapshot()
642+ return nil
643+}
644+
645+func (env *environ) ClosePorts(ports []instance.Port) error {
646+ logger.Warningf("pretending to close ports %v for all instances", ports)
647+ _ = env.getSnapshot()
648+ return nil
649+}
650+
651+func (env *environ) Ports() ([]instance.Port, error) {
652+ _ = env.getSnapshot()
653+ return nil, nil
654+}
655
656=== added file 'provider/skeleton/environ_instance.go'
657--- provider/skeleton/environ_instance.go 1970-01-01 00:00:00 +0000
658+++ provider/skeleton/environ_instance.go 2014-04-18 14:48:42 +0000
659@@ -0,0 +1,43 @@
660+// Copyright 2013 Canonical Ltd.
661+// Licensed under the AGPLv3, see LICENCE file for details.
662+
663+package skeleton
664+
665+import (
666+ "launchpad.net/juju-core/environs"
667+ "launchpad.net/juju-core/instance"
668+)
669+
670+func (env *environ) StartInstance(args environs.StartInstanceParams) (
671+ instance.Instance, *instance.HardwareCharacteristics, []environs.NetworkInfo, error,
672+) {
673+ // Please note that in order to fulfil the demands made of Instances and
674+ // AllInstances, it is imperative that some environment feature be used to
675+ // keep track of which instances were actually started by juju.
676+ _ = env.getSnapshot()
677+ return nil, nil, nil, errNotImplemented
678+}
679+
680+func (env *environ) AllInstances() ([]instance.Instance, error) {
681+ // Please note that this must *not* return instances that have not been
682+ // allocated as part of this environment -- if it does, juju will see they
683+ // are not tracked in state, assume they're stale/rogue, and shut them down.
684+ _ = env.getSnapshot()
685+ return nil, errNotImplemented
686+}
687+
688+func (env *environ) Instances(ids []instance.Id) ([]instance.Instance, error) {
689+ // Please note that this must *not* return instances that have not been
690+ // allocated as part of this environment -- if it does, juju will see they
691+ // are not tracked in state, assume they're stale/rogue, and shut them down.
692+ // This advice applies even if an instance id passed in corresponds to a
693+ // real instance that's not part of the environment -- the Environ should
694+ // treat that no differently to a request for one that does not exist.
695+ _ = env.getSnapshot()
696+ return nil, errNotImplemented
697+}
698+
699+func (env *environ) StopInstances(instances []instance.Instance) error {
700+ _ = env.getSnapshot()
701+ return errNotImplemented
702+}
703
704=== added file 'provider/skeleton/export_test.go'
705--- provider/skeleton/export_test.go 1970-01-01 00:00:00 +0000
706+++ provider/skeleton/export_test.go 2014-04-18 14:48:42 +0000
707@@ -0,0 +1,10 @@
708+// Copyright 2013 Canonical Ltd.
709+// Licensed under the AGPLv3, see LICENCE file for details.
710+
711+package skeleton
712+
713+import (
714+ "launchpad.net/juju-core/environs"
715+)
716+
717+var Provider environs.EnvironProvider = providerInstance
718
719=== added file 'provider/skeleton/instance.go'
720--- provider/skeleton/instance.go 1970-01-01 00:00:00 +0000
721+++ provider/skeleton/instance.go 2014-04-18 14:48:42 +0000
722@@ -0,0 +1,50 @@
723+// Copyright 2013 Canonical Ltd.
724+// Licensed under the AGPLv3, see LICENCE file for details.
725+
726+package skeleton
727+
728+import (
729+ "launchpad.net/juju-core/instance"
730+ "launchpad.net/juju-core/provider/common"
731+)
732+
733+type environInstance struct {
734+ id instance.Id
735+ env *environ
736+}
737+
738+var _ instance.Instance = (*environInstance)(nil)
739+
740+func (inst *environInstance) Id() instance.Id {
741+ return inst.id
742+}
743+
744+func (inst *environInstance) Status() string {
745+ _ = inst.env.getSnapshot()
746+ return "unknown (not implemented)"
747+}
748+
749+func (inst *environInstance) Refresh() error {
750+ _ = inst.env.getSnapshot()
751+ return errNotImplemented
752+}
753+
754+func (inst *environInstance) Addresses() ([]instance.Address, error) {
755+ _ = inst.env.getSnapshot()
756+ return nil, errNotImplemented
757+}
758+
759+func (inst *environInstance) DNSName() (string, error) {
760+ // This method is likely to be replaced entirely by Addresses() at some point,
761+ // but remains necessary for now. It's probably smart to implement it in
762+ // terms of Addresses above, to minimise churn when it's removed.
763+ _ = inst.env.getSnapshot()
764+ return "", errNotImplemented
765+}
766+
767+func (inst *environInstance) WaitDNSName() (string, error) {
768+ // This method is likely to be replaced entirely by Addresses() at some point,
769+ // but remains necessary for now. Until it's finally removed, you can probably
770+ // ignore this method; the common implementation should work.
771+ return common.WaitDNSName(inst)
772+}
773
774=== added file 'provider/skeleton/instance_firewall.go'
775--- provider/skeleton/instance_firewall.go 1970-01-01 00:00:00 +0000
776+++ provider/skeleton/instance_firewall.go 2014-04-18 14:48:42 +0000
777@@ -0,0 +1,26 @@
778+// Copyright 2013 Canonical Ltd.
779+// Licensed under the AGPLv3, see LICENCE file for details.
780+
781+package skeleton
782+
783+import (
784+ "launchpad.net/juju-core/instance"
785+)
786+
787+// Implementing the methods below (to do something other than return nil) will
788+// cause `juju expose` to work when the firewall-mode is "instance". If you
789+// implement one of them, you should implement them all.
790+
791+func (inst *environInstance) OpenPorts(machineId string, ports []instance.Port) error {
792+ logger.Warningf("pretending to open ports %v for instance %q", ports, inst.id)
793+ return nil
794+}
795+
796+func (inst *environInstance) ClosePorts(machineId string, ports []instance.Port) error {
797+ logger.Warningf("pretending to close ports %v for instance %q", ports, inst.id)
798+ return nil
799+}
800+
801+func (inst *environInstance) Ports(machineId string) ([]instance.Port, error) {
802+ return nil, nil
803+}
804
805=== added file 'provider/skeleton/provider.go'
806--- provider/skeleton/provider.go 1970-01-01 00:00:00 +0000
807+++ provider/skeleton/provider.go 2014-04-18 14:48:42 +0000
808@@ -0,0 +1,121 @@
809+// Copyright 2013 Canonical Ltd.
810+// Licensed under the AGPLv3, see LICENCE file for details.
811+
812+package skeleton
813+
814+import (
815+ "errors"
816+ "fmt"
817+
818+ "launchpad.net/loggo"
819+
820+ "launchpad.net/juju-core/environs"
821+ "launchpad.net/juju-core/environs/config"
822+)
823+
824+var logger = loggo.GetLogger("juju.provider.skeleton")
825+
826+type environProvider struct{}
827+
828+var providerInstance = environProvider{}
829+var _ environs.EnvironProvider = providerInstance
830+
831+func init() {
832+ // This will only happen in binaries that actually import this provider
833+ // somewhere. To enable a provider, import it in the "providers/all"
834+ // package; please do *not* import individual providers anywhere else,
835+ // except in direct tests for that provider.
836+ environs.RegisterProvider("skeleton", providerInstance)
837+}
838+
839+var errNotImplemented = errors.New("not implemented in skeleton provider")
840+
841+func (environProvider) Open(cfg *config.Config) (environs.Environ, error) {
842+ // You should probably not change this method; prefer to cause SetConfig
843+ // to completely configure an environment, regardless of the initial state.
844+ env := &environ{name: cfg.Name()}
845+ if err := env.SetConfig(cfg); err != nil {
846+ return nil, err
847+ }
848+ return env, nil
849+}
850+
851+func (environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) {
852+ // You should probably not change this method; if you need to change how
853+ // configs are prepared, you should edit prepareConfig directly, lest the
854+ // code in this file drift gradually out of sync with that in config.go
855+ cfg, err := prepareConfig(cfg)
856+ if err != nil {
857+ return nil, err
858+ }
859+ return providerInstance.Open(cfg)
860+}
861+
862+func (environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
863+ // You should almost certainly not change this method; if you need to change
864+ // how configs are validated, you should edit validateConfig itself, to ensure
865+ // that your checks are always applied.
866+ newEcfg, err := validateConfig(cfg, nil)
867+ if err != nil {
868+ return nil, fmt.Errorf("invalid config: %v", err)
869+ }
870+ if old != nil {
871+ oldEcfg, err := validateConfig(old, nil)
872+ if err != nil {
873+ return nil, fmt.Errorf("invalid base config: %v", err)
874+ }
875+ if newEcfg, err = validateConfig(cfg, oldEcfg); err != nil {
876+ return nil, fmt.Errorf("invalid config change: %v", err)
877+ }
878+ }
879+ return newEcfg.Config, nil
880+}
881+
882+func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
883+ // If you keep configSecretFields up to date, this method should Just Work.
884+ ecfg, err := validateConfig(cfg, nil)
885+ if err != nil {
886+ return nil, err
887+ }
888+ secretAttrs := map[string]string{}
889+ for _, field := range configSecretFields {
890+ if value, ok := ecfg.attrs[field]; ok {
891+ if stringValue, ok := value.(string); ok {
892+ secretAttrs[field] = stringValue
893+ } else {
894+ // All your secret attributes must be strings at the moment. Sorry.
895+ // It's an expedient and hopefully temporary measure that helps us
896+ // plug a security hole in the API.
897+ return nil, fmt.Errorf(
898+ "secret %q field must have a string value; got %v",
899+ field, value,
900+ )
901+ }
902+ }
903+ }
904+ return secretAttrs, nil
905+}
906+
907+func (environProvider) BoilerplateConfig() string {
908+ // boilerplateConfig is kept in config.go, in the hope that people editing
909+ // config will keep it up to date.
910+ return boilerplateConfig
911+}
912+
913+func (environProvider) PublicAddress() (string, error) {
914+ // Don't bother implementing this method until you're ready to deploy units.
915+ // You probably won't need to by that stage; it's due for retirement. If it
916+ // turns out that you do need to, remember that this method will *only* be
917+ // called in code running on an instance in an environment using this
918+ // provider; and it needs to return the address of *that* instance.
919+ return "", errNotImplemented
920+}
921+
922+func (environProvider) PrivateAddress() (string, error) {
923+ // Don't bother implementing this method until you're ready to deploy units.
924+ // You probably won't need to by that stage; it's due for retirement. If it
925+ // turns out that you do need to, remember that this method will *only* be
926+ // called in code running on an instance in an environment using this
927+ // provider; and it needs to return the address of *that* instance.
928+ return "", errNotImplemented
929+}
930
931=== added file 'provider/skeleton/skeleton_test.go'
932--- provider/skeleton/skeleton_test.go 1970-01-01 00:00:00 +0000
933+++ provider/skeleton/skeleton_test.go 2014-04-18 14:48:42 +0000
934@@ -0,0 +1,27 @@
935+// Copyright 2013 Canonical Ltd.
936+// Licensed under the AGPLv3, see LICENCE file for details.
937+
938+package skeleton_test
939+
940+import (
941+ "testing"
942+
943+ gc "launchpad.net/gocheck"
944+
945+ "launchpad.net/juju-core/environs"
946+ "launchpad.net/juju-core/provider/skeleton"
947+)
948+
949+func TestPackage(t *testing.T) {
950+ gc.TestingT(t)
951+}
952+
953+type SkeletonSuite struct{}
954+
955+var _ = gc.Suite(&SkeletonSuite{})
956+
957+func (*SkeletonSuite) TestRegistered(c *gc.C) {
958+ provider, err := environs.Provider("skeleton")
959+ c.Assert(err, gc.IsNil)
960+ c.Assert(provider, gc.Equals, skeleton.Provider)
961+}
962
963=== added file 'provider/skeleton/storage.go'
964--- provider/skeleton/storage.go 1970-01-01 00:00:00 +0000
965+++ provider/skeleton/storage.go 2014-04-18 14:48:42 +0000
966@@ -0,0 +1,53 @@
967+// Copyright 2013 Canonical Ltd.
968+// Licensed under the AGPLv3, see LICENCE file for details.
969+
970+package skeleton
971+
972+import (
973+ "io"
974+
975+ "launchpad.net/juju-core/environs/storage"
976+ "launchpad.net/juju-core/utils"
977+)
978+
979+type environStorage struct {
980+ ecfg *environConfig
981+}
982+
983+var _ storage.Storage = (*environStorage)(nil)
984+
985+func newStorage(ecfg *environConfig) (storage.Storage, error) {
986+ return &environStorage{ecfg}, nil
987+}
988+
989+func (s *environStorage) List(prefix string) ([]string, error) {
990+ return nil, errNotImplemented
991+}
992+
993+func (s *environStorage) URL(name string) (string, error) {
994+ return "", errNotImplemented
995+}
996+
997+func (s *environStorage) Get(name string) (io.ReadCloser, error) {
998+ return nil, errNotImplemented
999+}
1000+
1001+func (s *environStorage) Put(name string, r io.Reader, length int64) error {
1002+ return errNotImplemented
1003+}
1004+
1005+func (s *environStorage) Remove(name string) error {
1006+ return errNotImplemented
1007+}
1008+
1009+func (s *environStorage) RemoveAll() error {
1010+ return errNotImplemented
1011+}
1012+
1013+func (s *environStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
1014+ return utils.AttemptStrategy{}
1015+}
1016+
1017+func (s *environStorage) ShouldRetry(err error) bool {
1018+ return false
1019+}

Subscribers

People subscribed via source and target branches

to status/vote changes: