Skip to content

Commit 39f2156

Browse files
committed
Add initial support for controller charms (local ony for now)
1 parent 8db698e commit 39f2156

File tree

19 files changed

+404
-24
lines changed

19 files changed

+404
-24
lines changed

apiserver/charms.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func (h *charmsHandler) processPost(r *http.Request, st *state.State) (*charm.UR
295295

296296
// Now we need to repackage it with the reserved URL, upload it to
297297
// provider storage and update the state.
298-
err = h.repackageAndUploadCharm(st, archive, curl)
298+
err = RepackageAndUploadCharm(st, archive, curl)
299299
if err != nil {
300300
return nil, errors.Trace(err)
301301
}
@@ -389,10 +389,10 @@ func (d byDepth) Len() int { return len(d) }
389389
func (d byDepth) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
390390
func (d byDepth) Less(i, j int) bool { return depth(d[i]) < depth(d[j]) }
391391

392-
// repackageAndUploadCharm expands the given charm archive to a
392+
// RepackageAndUploadCharm expands the given charm archive to a
393393
// temporary directory, repackages it with the given curl's revision,
394394
// then uploads it to storage, and finally updates the state.
395-
func (h *charmsHandler) repackageAndUploadCharm(st *state.State, archive *charm.CharmArchive, curl *charm.URL) error {
395+
func RepackageAndUploadCharm(st *state.State, archive *charm.CharmArchive, curl *charm.URL) error {
396396
// Create a temp dir to contain the extracted charm dir.
397397
tempDir, err := ioutil.TempDir("", "charm-download")
398398
if err != nil {

apiserver/facades/client/application/application.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"github.com/juju/juju/core/permission"
4747
"github.com/juju/juju/core/status"
4848
"github.com/juju/juju/environs"
49+
"github.com/juju/juju/environs/bootstrap"
4950
"github.com/juju/juju/feature"
5051
"github.com/juju/juju/state"
5152
"github.com/juju/juju/state/stateenvirons"
@@ -1633,6 +1634,21 @@ func (api *APIBase) AddUnits(args params.AddApplicationUnits) (params.AddApplica
16331634
if api.modelType == state.ModelTypeCAAS {
16341635
return params.AddApplicationUnitsResults{}, errors.NotSupportedf("adding units on a non-container model")
16351636
}
1637+
1638+
// TODO(wallyworld) - enable-ha is how we add new controllers at the moment
1639+
// Remove this check before 3.0 when enable-ha is refactored.
1640+
app, err := api.backend.Application(args.ApplicationName)
1641+
if err != nil {
1642+
return params.AddApplicationUnitsResults{}, errors.Trace(err)
1643+
}
1644+
ch, _, err := app.Charm()
1645+
if err != nil {
1646+
return params.AddApplicationUnitsResults{}, errors.Trace(err)
1647+
}
1648+
if ch.Meta().Name == bootstrap.ControllerCharmName {
1649+
return params.AddApplicationUnitsResults{}, errors.NotSupportedf("add units to the controller application")
1650+
}
1651+
16361652
if err := api.checkCanWrite(); err != nil {
16371653
return params.AddApplicationUnitsResults{}, errors.Trace(err)
16381654
}
@@ -1762,11 +1778,14 @@ func (api *APIBase) DestroyUnit(args params.DestroyUnitsParams) (params.DestroyU
17621778
if err := api.check.RemoveAllowed(); err != nil {
17631779
return params.DestroyUnitResults{}, errors.Trace(err)
17641780
}
1781+
1782+
appCharms := make(map[string]Charm)
17651783
destroyUnit := func(arg params.DestroyUnitParams) (*params.DestroyUnitInfo, error) {
17661784
unitTag, err := names.ParseUnitTag(arg.UnitTag)
17671785
if err != nil {
17681786
return nil, errors.Trace(err)
17691787
}
1788+
17701789
name := unitTag.Id()
17711790
unit, err := api.backend.Unit(name)
17721791
if errors.IsNotFound(err) {
@@ -1777,6 +1796,26 @@ func (api *APIBase) DestroyUnit(args params.DestroyUnitsParams) (params.DestroyU
17771796
if !unit.IsPrincipal() {
17781797
return nil, errors.Errorf("unit %q is a subordinate", name)
17791798
}
1799+
1800+
// TODO(wallyworld) - enable-ha is how we remove controllers at the moment
1801+
// Remove this check before 3.0 when enable-ha is refactored.
1802+
appName, _ := names.UnitApplication(unitTag.Id())
1803+
ch, ok := appCharms[appName]
1804+
if !ok {
1805+
app, err := api.backend.Application(appName)
1806+
if err != nil {
1807+
return nil, errors.Trace(err)
1808+
}
1809+
ch, _, err = app.Charm()
1810+
if err != nil {
1811+
return nil, errors.Trace(err)
1812+
}
1813+
appCharms[appName] = ch
1814+
}
1815+
if ch.Meta().Name == bootstrap.ControllerCharmName {
1816+
return nil, errors.NotSupportedf("remove units from the controller application")
1817+
}
1818+
17801819
var info params.DestroyUnitInfo
17811820
unitStorage, err := storagecommon.UnitStorage(api.storageAccess, unit.UnitTag())
17821821
if err != nil {
@@ -1884,6 +1923,15 @@ func (api *APIBase) DestroyApplication(args params.DestroyApplicationsParams) (p
18841923
if err != nil {
18851924
return nil, err
18861925
}
1926+
1927+
ch, _, err := app.Charm()
1928+
if err != nil {
1929+
return nil, errors.Trace(err)
1930+
}
1931+
if ch.Meta().Name == bootstrap.ControllerCharmName {
1932+
return nil, errors.NotSupportedf("removing the controller application")
1933+
}
1934+
18871935
units, err := app.AllUnits()
18881936
if err != nil {
18891937
return nil, err

apiserver/facades/client/application/application_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2877,6 +2877,13 @@ func (s *applicationSuite) TestBlockApplicationDestroy(c *gc.C) {
28772877
}
28782878
}
28792879

2880+
func (s *applicationSuite) TestDestroyControllerApplicationNotAllowed(c *gc.C) {
2881+
s.AddTestingApplication(c, "controller-application", s.AddTestingCharm(c, "juju-controller"))
2882+
2883+
err := s.applicationAPI.Destroy(params.ApplicationDestroy{"controller-application"})
2884+
c.Assert(err, gc.ErrorMatches, "removing the controller application not supported")
2885+
}
2886+
28802887
func (s *applicationSuite) TestDestroyPrincipalUnits(c *gc.C) {
28812888
wordpress := s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
28822889
units := make([]*state.Unit, 5)

apiserver/facades/client/application/application_unit_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -851,13 +851,13 @@ func (s *ApplicationSuite) assertDestroyUnit(c *gc.C, force bool, maxWait *time.
851851

852852
s.backend.CheckCallNames(c,
853853
"Unit",
854+
"Application",
854855
"UnitStorageAttachments",
855856
"StorageInstance",
856857
"StorageInstance",
857858
"StorageInstanceFilesystem",
858859
"StorageInstanceFilesystem",
859860
"ApplyOperation",
860-
861861
"Unit",
862862
"UnitStorageAttachments",
863863
"ApplyOperation",
@@ -866,8 +866,8 @@ func (s *ApplicationSuite) assertDestroyUnit(c *gc.C, force bool, maxWait *time.
866866
if force {
867867
expectedOp.MaxWait = common.MaxWait(maxWait)
868868
}
869-
s.backend.CheckCall(c, 6, "ApplyOperation", expectedOp)
870-
s.backend.CheckCall(c, 9, "ApplyOperation", &state.DestroyUnitOperation{
869+
s.backend.CheckCall(c, 7, "ApplyOperation", expectedOp)
870+
s.backend.CheckCall(c, 10, "ApplyOperation", &state.DestroyUnitOperation{
871871
DestroyStorage: true,
872872
})
873873
}
@@ -1178,7 +1178,7 @@ func (s *ApplicationSuite) TestAddUnits(c *gc.C) {
11781178
Units: []string{"postgresql/99"},
11791179
})
11801180
app := s.backend.applications["postgresql"]
1181-
app.CheckCall(c, 0, "AddUnit", state.AddUnitParams{})
1181+
app.CheckCall(c, 1, "AddUnit", state.AddUnitParams{})
11821182
app.addedUnit.CheckCall(c, 0, "AssignWithPolicy", state.AssignCleanEmpty)
11831183
}
11841184

@@ -1356,23 +1356,23 @@ func (s *ApplicationSuite) TestAddUnitsAttachStorage(c *gc.C) {
13561356
c.Assert(err, jc.ErrorIsNil)
13571357

13581358
app := s.backend.applications["postgresql"]
1359-
app.CheckCall(c, 0, "AddUnit", state.AddUnitParams{
1359+
app.CheckCall(c, 1, "AddUnit", state.AddUnitParams{
13601360
AttachStorage: []names.StorageTag{names.NewStorageTag("pgdata/0")},
13611361
})
13621362
}
13631363

13641364
func (s *ApplicationSuite) TestAddUnitsAttachStorageMultipleUnits(c *gc.C) {
13651365
_, err := s.api.AddUnits(params.AddApplicationUnits{
1366-
ApplicationName: "foo",
1366+
ApplicationName: "postgresql",
13671367
NumUnits: 2,
1368-
AttachStorage: []string{"storage-foo-0"},
1368+
AttachStorage: []string{"storage-postgresql-0"},
13691369
})
13701370
c.Assert(err, gc.ErrorMatches, "AttachStorage is non-empty, but NumUnits is 2")
13711371
}
13721372

13731373
func (s *ApplicationSuite) TestAddUnitsAttachStorageInvalidStorageTag(c *gc.C) {
13741374
_, err := s.api.AddUnits(params.AddApplicationUnits{
1375-
ApplicationName: "foo",
1375+
ApplicationName: "postgresql",
13761376
NumUnits: 1,
13771377
AttachStorage: []string{"volume-0"},
13781378
})

apiserver/facades/client/application/deploy.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/juju/juju/core/constraints"
1717
"github.com/juju/juju/core/devices"
1818
"github.com/juju/juju/core/instance"
19+
"github.com/juju/juju/environs/bootstrap"
1920
"github.com/juju/juju/state"
2021
"github.com/juju/juju/storage"
2122
)
@@ -57,6 +58,9 @@ func DeployApplication(st ApplicationDeployer, args DeployApplicationParams) (Ap
5758
if err != nil {
5859
return nil, errors.Trace(err)
5960
}
61+
if args.Charm.Meta().Name == bootstrap.ControllerCharmName {
62+
return nil, errors.NotSupportedf("manual deploy of the controller charm")
63+
}
6064
if args.Charm.Meta().Subordinate {
6165
if args.NumUnits != 0 {
6266
return nil, fmt.Errorf("subordinate application must be deployed without units")

apiserver/facades/client/application/deploy_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ func (s *DeployLocalSuite) SetUpTest(c *gc.C) {
4747
s.charm = charm
4848
}
4949

50+
func (s *DeployLocalSuite) TestDeployControllerNotAllowed(c *gc.C) {
51+
ch := s.AddTestingCharm(c, "juju-controller")
52+
_, err := application.DeployApplication(stateDeployer{s.State},
53+
application.DeployApplicationParams{
54+
ApplicationName: "my-controller",
55+
Charm: ch,
56+
})
57+
c.Assert(err, gc.ErrorMatches, "manual deploy of the controller charm not supported")
58+
}
59+
5060
func (s *DeployLocalSuite) TestDeployMinimal(c *gc.C) {
5161
app, err := application.DeployApplication(stateDeployer{s.State},
5262
application.DeployApplicationParams{

cloudconfig/instancecfg/instancecfg.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ type BootstrapConfig struct {
211211
// Dashboard is the Juju Dashboard archive to be installed in the new instance.
212212
Dashboard *coretools.DashboardArchive
213213

214+
// ControllerCharm is a local controller charm to be used.
215+
ControllerCharm string
216+
214217
// Timeout is the amount of time to wait for bootstrap to complete.
215218
Timeout time.Duration
216219

@@ -490,6 +493,11 @@ func (cfg *InstanceConfig) SnapDir() string {
490493
return path.Join(cfg.DataDir, "snap")
491494
}
492495

496+
// CharmDir returns the directory where system charms should be uploaded to.
497+
func (cfg *InstanceConfig) CharmDir() string {
498+
return path.Join(cfg.DataDir, "charms")
499+
}
500+
493501
// DashboardDir returns the directory where the Juju Dashboard release is stored.
494502
func (cfg *InstanceConfig) DashboardDir() string {
495503
return agenttools.SharedDashboardDir(cfg.DataDir)
@@ -608,10 +616,22 @@ func (cfg *InstanceConfig) SetSnapSource(snapPath string, snapAssertionsPath str
608616
return nil
609617
}
610618

611-
type requiresError string
619+
// SetControllerCharm annotates the instance configuration
620+
// with the location of a local controller charm to upload during
621+
// the instance's provisioning.
622+
func (cfg *InstanceConfig) SetControllerCharm(controllerCharmPath string) error {
623+
if controllerCharmPath == "" {
624+
return nil
625+
}
612626

613-
func (e requiresError) Error() string {
614-
return "invalid machine configuration: missing " + string(e)
627+
_, err := os.Stat(controllerCharmPath)
628+
if err != nil {
629+
return errors.Annotatef(err, "unable set local controller charm (at %s)", controllerCharmPath)
630+
}
631+
632+
cfg.Bootstrap.ControllerCharm = controllerCharmPath
633+
634+
return nil
615635
}
616636

617637
// VerifyConfig verifies that the InstanceConfig is valid.

cloudconfig/instancecfg/instancecfg_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,10 @@ func (*instancecfgSuite) TestDashboardDir(c *gc.C) {
111111
}
112112
c.Assert(icfg.DashboardDir(), gc.Equals, "/path/to/datadir/dashboard")
113113
}
114+
115+
func (*instancecfgSuite) TestCharmDir(c *gc.C) {
116+
icfg := &instancecfg.InstanceConfig{
117+
DataDir: "/path/to/datadir/",
118+
}
119+
c.Assert(icfg.CharmDir(), gc.Equals, "/path/to/datadir/charms")
120+
}

cloudconfig/userdatacfg_test.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import (
99
"encoding/json"
1010
"fmt"
1111
"io/ioutil"
12+
"os"
1213
"path"
1314
"path/filepath"
1415
"regexp"
1516
"strings"
1617
"time"
1718

19+
"github.com/juju/charm/v9"
1820
"github.com/juju/collections/set"
1921
"github.com/juju/loggo"
2022
"github.com/juju/names/v4"
@@ -37,6 +39,7 @@ import (
3739
"github.com/juju/juju/environs/config"
3840
"github.com/juju/juju/environs/imagemetadata"
3941
jujutesting "github.com/juju/juju/juju/testing"
42+
"github.com/juju/juju/testcharms"
4043
"github.com/juju/juju/testing"
4144
"github.com/juju/juju/tools"
4245
)
@@ -175,6 +178,11 @@ func (cfg *testInstanceConfig) setDashboard(url string) *testInstanceConfig {
175178
return cfg
176179
}
177180

181+
func (cfg *testInstanceConfig) setControllerCharm(path string) *testInstanceConfig {
182+
cfg.Bootstrap.ControllerCharm = path
183+
return cfg
184+
}
185+
178186
// maybeSetModelConfig sets the Config field to the given envConfig, if not
179187
// nil, and the instance config is for a bootstrap machine.
180188
func (cfg *testInstanceConfig) maybeSetModelConfig(envConfig *config.Config) *testInstanceConfig {
@@ -715,7 +723,7 @@ printf %%s %s | base64 -d > '/var/lib/juju/dashboard/dashboard.tar.bz2'
715723
[ -f $dashboard/jujudashboard.sha256 ] && (grep '1234' $dashboard/jujudashboard.sha256 && printf %%s '%s' > $dashboard/downloaded-dashboard.txt || echo Juju Dashboard checksum mismatch)
716724
rm -f $dashboard/dashboard.tar.bz2 $dashboard/jujudashboard.sha256 $dashboard/downloaded-dashboard.txt
717725
`, base64Content, dashboardJson))
718-
checkCloudInitWithDashboard(c, cfg, expectedScripts, "")
726+
checkCloudInitWithContent(c, cfg, expectedScripts, "")
719727
}
720728

721729
func (*cloudinitSuite) TestCloudInitWithRemoteDashboard(c *gc.C) {
@@ -729,22 +737,22 @@ curl -sSf -o $dashboard/dashboard.tar.bz2 --retry 10 'https://1.2.3.4/dashboard.
729737
[ -f $dashboard/jujudashboard.sha256 ] && (grep '1234' $dashboard/jujudashboard.sha256 && printf %%s '%s' > $dashboard/downloaded-dashboard.txt || echo Juju Dashboard checksum mismatch)
730738
rm -f $dashboard/dashboard.tar.bz2 $dashboard/jujudashboard.sha256 $dashboard/downloaded-dashboard.txt
731739
`, dashboardJson))
732-
checkCloudInitWithDashboard(c, cfg, expectedScripts, "")
740+
checkCloudInitWithContent(c, cfg, expectedScripts, "")
733741
}
734742

735743
func (*cloudinitSuite) TestCloudInitWithDashboardReadError(c *gc.C) {
736744
cfg := makeBootstrapConfig("precise", 0).setDashboard("file:///no/such/dashboard.tar.bz2")
737745
expectedError := "cannot set up Juju Dashboard: cannot read Juju Dashboard archive: .*"
738-
checkCloudInitWithDashboard(c, cfg, "", expectedError)
746+
checkCloudInitWithContent(c, cfg, "", expectedError)
739747
}
740748

741749
func (*cloudinitSuite) TestCloudInitWithDashboardURLError(c *gc.C) {
742750
cfg := makeBootstrapConfig("precise", 0).setDashboard(":")
743751
expectedError := "cannot set up Juju Dashboard: cannot parse Juju Dashboard URL: .*"
744-
checkCloudInitWithDashboard(c, cfg, "", expectedError)
752+
checkCloudInitWithContent(c, cfg, "", expectedError)
745753
}
746754

747-
func checkCloudInitWithDashboard(c *gc.C, cfg *testInstanceConfig, expectedScripts string, expectedError string) {
755+
func checkCloudInitWithContent(c *gc.C, cfg *testInstanceConfig, expectedScripts string, expectedError string) {
748756
envConfig := minimalModelConfig(c)
749757
testConfig := cfg.maybeSetModelConfig(envConfig).render()
750758
ci, err := cloudinit.New(testConfig.Series)
@@ -769,6 +777,30 @@ func checkCloudInitWithDashboard(c *gc.C, cfg *testInstanceConfig, expectedScrip
769777
assertScriptMatch(c, scripts, expectedScripts, false)
770778
}
771779

780+
func (*cloudinitSuite) TestCloudInitWithLocalControllerCharm(c *gc.C) {
781+
ch := testcharms.RepoForSeries("quantal").CharmDir("juju-controller")
782+
dir, err := charm.ReadCharmDir(ch.Path)
783+
c.Assert(err, jc.ErrorIsNil)
784+
785+
controllerCharmPath := filepath.Join(c.MkDir(), "controller.charm")
786+
f, err := os.Create(controllerCharmPath)
787+
c.Assert(err, jc.ErrorIsNil)
788+
err = dir.ArchiveTo(f)
789+
_ = f.Close()
790+
c.Assert(err, jc.ErrorIsNil)
791+
792+
content, err := ioutil.ReadFile(controllerCharmPath)
793+
c.Assert(err, jc.ErrorIsNil)
794+
795+
cfg := makeBootstrapConfig("precise", 0).setControllerCharm(controllerCharmPath)
796+
base64Content := base64.StdEncoding.EncodeToString(content)
797+
expectedScripts := regexp.QuoteMeta(fmt.Sprintf(`chmod 0600 '/var/lib/juju/agents/machine-0/agent.conf'
798+
install -D -m 644 /dev/null '/var/lib/juju/charms/controller.charm'
799+
printf %%s %s | base64 -d > '/var/lib/juju/charms/controller.charm'
800+
`, base64Content))
801+
checkCloudInitWithContent(c, cfg, expectedScripts, "")
802+
}
803+
772804
func (*cloudinitSuite) TestCloudInitConfigure(c *gc.C) {
773805
for i, test := range cloudinitTests {
774806
testConfig := test.cfg.maybeSetModelConfig(minimalModelConfig(c)).render()

0 commit comments

Comments
 (0)