Skip to content

Commit

Permalink
Merge pull request juju#18860 from nvinuesa/implement_app_constraints…
Browse files Browse the repository at this point in the history
…_migration

juju#18860

This patch adds model migration for domain application constraints.

Note: The migration import does not use the validation that is used when setting constraints in the app service. The reason is that we don't have a provider for migration yet. The validation will be added in a follow-up patch.


## QA steps


 1. Bootstrap a 3.6 controller and deploy a charm.

```sh
$ juju bootstrap lxd src36
$ juju add-model moveme1
$ juju deploy juju-qa-test
$ juju set-constraints juju-qa-test spaces=^alpha cpu-cores=2
```

 2. Bootstrap a 4.0 controller with the changes and migrate the model.

```sh
$ juju bootstrap lxd dst40
$ juju migrate src36:moveme1 dst40
$ juju add-unit juju-qa-test
$ juju constraints juju-qa-test
cores=2 spaces=^alpha
```

 3. Verify no errors exist in the model logs for the agents. If there are
 errors, this is a bug and should be fixed before merging. The fix can
 either be applied to the 4.0 branch (preferable) or the 3.6 branch, though
 that needs to be discussed with the team.

```sh
$ juju debug-log -m dst40:controller
$ juju debug-log -m dst40:moveme1
```

 4. We also need to test a model migration from 4.0 to 4.0.

```sh
$ juju bootstrap lxd src40
$ juju add-model moveme2
$ juju deploy juju-qa-test
$ juju set-constraints juju-qa-test spaces=^alpha cpu-cores=2
```

```sh
$ juju migrate src40:moveme2 dst40
$ juju add-unit juju-qa-test
$ juju constraints juju-qa-test
cores=2 spaces=^alpha
```

 5. Verify that there are no errors in the controller or model logs.

```sh
$ juju debug-log -m dst40:controller
$ juju debug-log -m dst40:moveme2
```

## Links


**Jira card:** JUJU-7420
  • Loading branch information
jujubot authored Feb 26, 2025
2 parents 9001eab + 30fb940 commit 29174a6
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 8 deletions.
62 changes: 62 additions & 0 deletions domain/application/modelmigration/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
coreapplication "github.com/juju/juju/core/application"
corecharm "github.com/juju/juju/core/charm"
"github.com/juju/juju/core/config"
"github.com/juju/juju/core/constraints"
"github.com/juju/juju/core/logger"
"github.com/juju/juju/core/modelmigration"
corestatus "github.com/juju/juju/core/status"
Expand Down Expand Up @@ -68,6 +69,14 @@ type ExportService interface {
// If the application does not exist, a [applicationerrors.ApplicationNotFound]
// error is returned.
GetApplicationStatus(ctx context.Context, name string) (*corestatus.StatusInfo, error)

// GetApplicationConstraints returns the application constraints for the
// specified application name.
// Empty constraints are returned if no constraints exist for the given
// application ID.
// If no application is found, an error satisfying
// [applicationerrors.ApplicationNotFound] is returned.
GetApplicationConstraints(ctx context.Context, name string) (constraints.Value, error)
}

// exportOperation describes a way to execute a migration for
Expand Down Expand Up @@ -153,6 +162,12 @@ func (e *exportOperation) Execute(ctx context.Context, model description.Model)
if err := e.exportCharm(ctx, app, charm); err != nil {
return errors.Trace(err)
}

appCons, err := e.service.GetApplicationConstraints(ctx, app.Name())
if err != nil {
return fmt.Errorf("getting application constraints %q: %v", app.Name(), err)
}
app.SetConstraints(e.exportApplicationConstraints(appCons))
}
return nil
}
Expand Down Expand Up @@ -345,6 +360,53 @@ func (e *exportOperation) exportCharmActions(actions *internalcharm.Actions) (de
}, nil
}

func (e *exportOperation) exportApplicationConstraints(cons constraints.Value) description.ConstraintsArgs {
result := description.ConstraintsArgs{}
if cons.AllocatePublicIP != nil {
result.AllocatePublicIP = *cons.AllocatePublicIP
}
if cons.Arch != nil {
result.Architecture = *cons.Arch
}
if cons.Container != nil {
result.Container = string(*cons.Container)
}
if cons.CpuCores != nil {
result.CpuCores = *cons.CpuCores
}
if cons.CpuPower != nil {
result.CpuPower = *cons.CpuPower
}
if cons.Mem != nil {
result.Memory = *cons.Mem
}
if cons.RootDisk != nil {
result.RootDisk = *cons.RootDisk
}
if cons.RootDiskSource != nil {
result.RootDiskSource = *cons.RootDiskSource
}
if cons.ImageID != nil {
result.ImageID = *cons.ImageID
}
if cons.InstanceType != nil {
result.InstanceType = *cons.InstanceType
}
if cons.VirtType != nil {
result.VirtType = *cons.VirtType
}
if cons.Spaces != nil {
result.Spaces = *cons.Spaces
}
if cons.Tags != nil {
result.Tags = *cons.Tags
}
if cons.Zones != nil {
result.Zones = *cons.Zones
}
return result
}

const (
// Convert the charm-user to a string representation. This is a string
// representation of the internalcharm.RunAs type. This is done to ensure
Expand Down
69 changes: 69 additions & 0 deletions domain/application/modelmigration/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
gc "gopkg.in/check.v1"

"github.com/juju/juju/core/config"
"github.com/juju/juju/core/constraints"
"github.com/juju/juju/core/instance"
corestatus "github.com/juju/juju/core/status"
"github.com/juju/juju/domain/application"
"github.com/juju/juju/domain/application/charm"
Expand Down Expand Up @@ -64,6 +66,7 @@ func (s *exportSuite) TestApplicationExportMinimalCharm(c *gc.C) {
s.expectMinimalCharm()
s.expectApplicationConfig()
s.expectApplicationStatus()
s.expectApplicationConstraints(constraints.Value{})

exportOp := exportOperation{
service: s.exportService,
Expand All @@ -83,6 +86,68 @@ func (s *exportSuite) TestApplicationExportMinimalCharm(c *gc.C) {
c.Check(metadata.Name(), gc.Equals, "prometheus")
}

func (s *exportSuite) TestApplicationExportConstraints(c *gc.C) {
defer s.setupMocks(c).Finish()

model := description.NewModel(description.ModelArgs{})

appArgs := description.ApplicationArgs{
Tag: names.NewApplicationTag("prometheus"),
CharmURL: "ch:prometheus-1",
}
app := model.AddApplication(appArgs)
app.AddUnit(description.UnitArgs{
Tag: names.NewUnitTag("prometheus/0"),
})

s.expectApplicationStatus()
s.expectMinimalCharm()
s.expectApplicationConfig()
cons := constraints.Value{
AllocatePublicIP: ptr(true),
Arch: ptr("amd64"),
Container: ptr(instance.ContainerType("lxd")),
CpuCores: ptr(uint64(2)),
CpuPower: ptr(uint64(1000)),
ImageID: ptr("foo"),
InstanceRole: ptr("bar"),
InstanceType: ptr("baz"),
VirtType: ptr("vm"),
Mem: ptr(uint64(1024)),
RootDisk: ptr(uint64(1024)),
RootDiskSource: ptr("qux"),
Spaces: ptr([]string{"space0", "space1"}),
Tags: ptr([]string{"tag0", "tag1"}),
Zones: ptr([]string{"zone0", "zone1"}),
}
s.expectApplicationConstraints(cons)

exportOp := exportOperation{
service: s.exportService,
clock: clock.WallClock,
}

err := exportOp.Execute(context.Background(), model)
c.Assert(err, jc.ErrorIsNil)
c.Assert(model.Applications(), gc.HasLen, 1)

app = model.Applications()[0]
c.Check(app.Constraints().AllocatePublicIP(), gc.Equals, true)
c.Check(app.Constraints().Architecture(), gc.Equals, "amd64")
c.Check(app.Constraints().Container(), gc.Equals, "lxd")
c.Check(app.Constraints().CpuCores(), gc.Equals, uint64(2))
c.Check(app.Constraints().CpuPower(), gc.Equals, uint64(1000))
c.Check(app.Constraints().ImageID(), gc.Equals, "foo")
c.Check(app.Constraints().InstanceType(), gc.Equals, "baz")
c.Check(app.Constraints().VirtType(), gc.Equals, "vm")
c.Check(app.Constraints().Memory(), gc.Equals, uint64(1024))
c.Check(app.Constraints().RootDisk(), gc.Equals, uint64(1024))
c.Check(app.Constraints().RootDiskSource(), gc.Equals, "qux")
c.Check(app.Constraints().Spaces(), gc.DeepEquals, []string{"space0", "space1"})
c.Check(app.Constraints().Tags(), gc.DeepEquals, []string{"tag0", "tag1"})
c.Check(app.Constraints().Zones(), gc.DeepEquals, []string{"zone0", "zone1"})
}

func (s *exportSuite) TestExportCharmMetadata(c *gc.C) {
defer s.setupMocks(c).Finish()

Expand Down Expand Up @@ -428,3 +493,7 @@ func (s *exportSuite) expectApplicationStatus() {
Status: corestatus.Running,
}, nil)
}

func (s *exportSuite) expectApplicationConstraints(cons constraints.Value) {
s.exportService.EXPECT().GetApplicationConstraints(gomock.Any(), "prometheus").Return(cons, nil)
}
69 changes: 63 additions & 6 deletions domain/application/modelmigration/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
coreapplication "github.com/juju/juju/core/application"
corecharm "github.com/juju/juju/core/charm"
"github.com/juju/juju/core/config"
"github.com/juju/juju/core/constraints"
"github.com/juju/juju/core/instance"
"github.com/juju/juju/core/logger"
"github.com/juju/juju/core/modelmigration"
"github.com/juju/juju/core/network"
Expand Down Expand Up @@ -157,12 +159,13 @@ func (i *importOperation) Execute(ctx context.Context, model description.Model)
applicationStatus := i.importStatus(app.Status())

err = i.service.ImportApplication(ctx, app.Name(), service.ImportApplicationArgs{
Charm: charm,
CharmOrigin: origin,
Units: unitArgs,
ApplicationConfig: applicationConfig,
ApplicationSettings: applicationSettings,
ApplicationStatus: &applicationStatus,
Charm: charm,
CharmOrigin: origin,
Units: unitArgs,
ApplicationConfig: applicationConfig,
ApplicationSettings: applicationSettings,
ApplicationStatus: &applicationStatus,
ApplicationConstraints: i.importApplicationConstraints(app),

// ReferenceName is the name of the charm URL, not the application
// name and not the charm name in the metadata, but the name of
Expand Down Expand Up @@ -289,6 +292,60 @@ func (i *importOperation) importStatus(s description.Status) corestatus.StatusIn
}
}

func (i *importOperation) importApplicationConstraints(app description.Application) constraints.Value {
result := constraints.Value{}

cons := app.Constraints()
if cons == nil {
return result
}

if allocate := cons.AllocatePublicIP(); allocate {
result.AllocatePublicIP = &allocate
}
if arch := cons.Architecture(); arch != "" {
result.Arch = &arch
}
if container := instance.ContainerType(cons.Container()); container != "" {
result.Container = &container
}
if cores := cons.CpuCores(); cores != 0 {
result.CpuCores = &cores
}
if power := cons.CpuPower(); power != 0 {
result.CpuPower = &power
}
if inst := cons.InstanceType(); inst != "" {
result.InstanceType = &inst
}
if mem := cons.Memory(); mem != 0 {
result.Mem = &mem
}
if imageID := cons.ImageID(); imageID != "" {
result.ImageID = &imageID
}
if disk := cons.RootDisk(); disk != 0 {
result.RootDisk = &disk
}
if source := cons.RootDiskSource(); source != "" {
result.RootDiskSource = &source
}
if spaces := cons.Spaces(); len(spaces) > 0 {
result.Spaces = &spaces
}
if tags := cons.Tags(); len(tags) > 0 {
result.Tags = &tags
}
if virt := cons.VirtType(); virt != "" {
result.VirtType = &virt
}
if zones := cons.Zones(); len(zones) > 0 {
result.Zones = &zones
}

return result
}

// importCharmOrigin returns the charm origin for an application
//
// Ensure ID, Hash and Channel are dropped from local charm.
Expand Down
79 changes: 79 additions & 0 deletions domain/application/modelmigration/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
gc "gopkg.in/check.v1"

"github.com/juju/juju/core/config"
"github.com/juju/juju/core/instance"
"github.com/juju/juju/core/network"
"github.com/juju/juju/core/status"
"github.com/juju/juju/domain/application"
Expand Down Expand Up @@ -355,6 +356,84 @@ func (s *importSuite) TestApplicationImportWithApplicationStatusNotSet(c *gc.C)
})
}

func (s *importSuite) TestApplicationImportWithConstraints(c *gc.C) {
defer s.setupMocks(c).Finish()

model := description.NewModel(description.ModelArgs{})

appArgs := description.ApplicationArgs{
Tag: names.NewApplicationTag("prometheus"),
CharmURL: "ch:prometheus-1",
}
app := model.AddApplication(appArgs)
app.SetCharmMetadata(description.CharmMetadataArgs{
Name: "prometheus",
})
app.SetCharmManifest(description.CharmManifestArgs{
Bases: []description.CharmManifestBase{baseType{
name: "ubuntu",
channel: "24.04",
architectures: []string{"amd64"},
}},
})
app.SetCharmOrigin(description.CharmOriginArgs{
Source: "charm-hub",
ID: "1234",
Hash: "deadbeef",
Revision: 1,
Channel: "666/stable",
Platform: "arm64/ubuntu/24.04",
})

app.SetConstraints(description.ConstraintsArgs{
AllocatePublicIP: true,
Architecture: "amd64",
Container: "lxd",
CpuCores: uint64(2),
CpuPower: uint64(1000),
ImageID: "foo",
InstanceType: "baz",
VirtType: "vm",
Memory: uint64(1024),
RootDisk: uint64(1024),
RootDiskSource: "qux",
Spaces: []string{"space0", "space1"},
Tags: []string{"tag0", "tag1"},
Zones: []string{"zone0", "zone1"},
})

s.importService.EXPECT().ImportApplication(
gomock.Any(),
"prometheus",
gomock.Any(),
).DoAndReturn(func(_ context.Context, _ string, args service.ImportApplicationArgs) error {
c.Assert(args.Charm.Meta().Name, gc.Equals, "prometheus")
c.Check(args.ApplicationConstraints.AllocatePublicIP, gc.DeepEquals, ptr(true))
c.Check(args.ApplicationConstraints.Arch, gc.DeepEquals, ptr("amd64"))
c.Check(args.ApplicationConstraints.Container, gc.DeepEquals, ptr(instance.ContainerType("lxd")))
c.Check(args.ApplicationConstraints.CpuCores, gc.DeepEquals, ptr(uint64(2)))
c.Check(args.ApplicationConstraints.CpuPower, gc.DeepEquals, ptr(uint64(1000)))
c.Check(args.ApplicationConstraints.ImageID, gc.DeepEquals, ptr("foo"))
c.Check(args.ApplicationConstraints.InstanceType, gc.DeepEquals, ptr("baz"))
c.Check(args.ApplicationConstraints.VirtType, gc.DeepEquals, ptr("vm"))
c.Check(args.ApplicationConstraints.Mem, gc.DeepEquals, ptr(uint64(1024)))
c.Check(args.ApplicationConstraints.RootDisk, gc.DeepEquals, ptr(uint64(1024)))
c.Check(args.ApplicationConstraints.RootDiskSource, gc.DeepEquals, ptr("qux"))
c.Check(args.ApplicationConstraints.Spaces, gc.DeepEquals, ptr([]string{"space0", "space1"}))
c.Check(args.ApplicationConstraints.Tags, gc.DeepEquals, ptr([]string{"tag0", "tag1"}))
c.Check(args.ApplicationConstraints.Zones, gc.DeepEquals, ptr([]string{"zone0", "zone1"}))
return nil
})

importOp := importOperation{
service: s.importService,
logger: loggertesting.WrapCheckLog(c),
}

err := importOp.Execute(context.Background(), model)
c.Assert(err, jc.ErrorIsNil)
}

func (s *importSuite) TestImportCharmMetadataEmpty(c *gc.C) {
defer s.setupMocks(c).Finish()

Expand Down
Loading

0 comments on commit 29174a6

Please sign in to comment.