diff --git a/environs/imagemetadata/simplestreams.go b/environs/imagemetadata/simplestreams.go index 31dc42d961e..7d8a277551e 100644 --- a/environs/imagemetadata/simplestreams.go +++ b/environs/imagemetadata/simplestreams.go @@ -157,12 +157,8 @@ const ( ReleasedStream = "released" ) -// ImageRelease maps a legacy series to an image version. -func ImageRelease(imSeries string) (string, error) { - base, err := corebase.GetBaseFromSeries(imSeries) - if err != nil { - return "", errors.Trace(err) - } +// ImageRelease maps a base to an image version. +func ImageRelease(base corebase.Base) (string, error) { if base.OS != "centos" { return base.Channel.Track, nil } diff --git a/internal/provider/ec2/environ.go b/internal/provider/ec2/environ.go index 049ee0c8c38..8f430d80453 100644 --- a/internal/provider/ec2/environ.go +++ b/internal/provider/ec2/environ.go @@ -31,7 +31,6 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/controller" "github.com/juju/juju/core/arch" - corebase "github.com/juju/juju/core/base" "github.com/juju/juju/core/constraints" corecontext "github.com/juju/juju/core/context" "github.com/juju/juju/core/instance" @@ -490,12 +489,7 @@ func (e *environ) AgentMetadataLookupParams(region string) (*simplestreams.Metad // ImageMetadataLookupParams returns parameters which are used to query image simple-streams metadata. func (e *environ) ImageMetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { base := config.PreferredBase(e.ecfg()) - baseSeries, err := corebase.GetSeriesFromBase(base) - if err != nil { - return nil, errors.Trace(err) - } - - release, err := imagemetadata.ImageRelease(baseSeries) + release, err := imagemetadata.ImageRelease(base) if err != nil { return nil, errors.Trace(err) } diff --git a/internal/provider/openstack/provider.go b/internal/provider/openstack/provider.go index cec7890d4b3..caaf34584be 100644 --- a/internal/provider/openstack/provider.go +++ b/internal/provider/openstack/provider.go @@ -39,7 +39,6 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/cmd/juju/interact" - corebase "github.com/juju/juju/core/base" "github.com/juju/juju/core/constraints" "github.com/juju/juju/core/instance" "github.com/juju/juju/core/network" @@ -2289,12 +2288,7 @@ func (e *Environ) AgentMetadataLookupParams(region string) (*simplestreams.Metad // ImageMetadataLookupParams returns parameters which are used to query image simple-streams metadata. func (e *Environ) ImageMetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { base := config.PreferredBase(e.ecfg()) - baseSeries, err := corebase.GetSeriesFromBase(base) - if err != nil { - return nil, errors.Trace(err) - } - - release, err := imagemetadata.ImageRelease(baseSeries) + release, err := imagemetadata.ImageRelease(base) if err != nil { return nil, errors.Trace(err) } diff --git a/internal/provider/vsphere/image_metadata.go b/internal/provider/vsphere/image_metadata.go index 894b4e0c27f..b359d7c1876 100644 --- a/internal/provider/vsphere/image_metadata.go +++ b/internal/provider/vsphere/image_metadata.go @@ -8,6 +8,7 @@ import ( "github.com/juju/errors" + "github.com/juju/juju/core/base" "github.com/juju/juju/environs" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/environs/simplestreams" @@ -33,8 +34,8 @@ func init() { simplestreams.RegisterStructTags(OvaFileMetadata{}) } -func findImageMetadata(ctx context.Context, env environs.Environ, arch string, series string) (*OvaFileMetadata, error) { - vers, err := imagemetadata.ImageRelease(series) +func findImageMetadata(ctx context.Context, env environs.Environ, arch string, b base.Base) (*OvaFileMetadata, error) { + vers, err := imagemetadata.ImageRelease(b) if err != nil { return nil, errors.Trace(err) } diff --git a/internal/provider/vsphere/vm_template.go b/internal/provider/vsphere/vm_template.go index f0ed5889b87..362af336ac4 100644 --- a/internal/provider/vsphere/vm_template.go +++ b/internal/provider/vsphere/vm_template.go @@ -14,6 +14,7 @@ import ( "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/types" + "github.com/juju/juju/core/base" "github.com/juju/juju/environs" "github.com/juju/juju/environs/imagemetadata" "github.com/juju/juju/internal/provider/vsphere/internal/vsphereclient" @@ -181,7 +182,11 @@ func (v *vmTemplateManager) downloadAndImportTemplate( if err != nil { return nil, "", errors.Trace(err) } - img, err := findImageMetadata(ctx, v.env, arch, series) + b, err := base.GetBaseFromSeries(series) + if err != nil { + return nil, "", environs.ZoneIndependentError(err) + } + img, err := findImageMetadata(ctx, v.env, arch, b) if err != nil { return nil, "", environs.ZoneIndependentError(err) } diff --git a/internal/worker/uniter/container/workload.go b/internal/worker/uniter/container/workload.go index 379baf96b2e..42fee05a273 100644 --- a/internal/worker/uniter/container/workload.go +++ b/internal/worker/uniter/container/workload.go @@ -33,6 +33,7 @@ const ( // ReadyEvent is triggered when the container/pebble starts up. ReadyEvent WorkloadEventType = iota CustomNoticeEvent + ChangeUpdatedEvent ) // WorkloadEvent contains information about the event type and data associated with @@ -199,6 +200,14 @@ func (r *workloadHookResolver) NextOp( NoticeType: evt.NoticeType, NoticeKey: evt.NoticeKey, }) + case ChangeUpdatedEvent: + op, err = opFactory.NewRunHook(hook.Info{ + Kind: hooks.PebbleChangeUpdated, + WorkloadName: evt.WorkloadName, + NoticeID: evt.NoticeID, + NoticeType: evt.NoticeType, + NoticeKey: evt.NoticeKey, + }) case ReadyEvent: op, err = opFactory.NewRunHook(hook.Info{ Kind: hooks.PebbleReady, diff --git a/internal/worker/uniter/container/workload_test.go b/internal/worker/uniter/container/workload_test.go index 776fbf69e93..eb100bd72e2 100644 --- a/internal/worker/uniter/container/workload_test.go +++ b/internal/worker/uniter/container/workload_test.go @@ -133,3 +133,46 @@ func (s *workloadSuite) TestWorkloadCustomNoticeHook(c *gc.C) { NoticeKey: "example.com/foo", }) } + +func (s *workloadSuite) TestWorkloadChangeUpdatedHook(c *gc.C) { + events := container.NewWorkloadEvents() + expectedErr := errors.Errorf("expected error") + handler := func(err error) { + c.Assert(err, gc.Equals, expectedErr) + } + containerResolver := container.NewWorkloadHookResolver( + loggo.GetLogger("test"), + events, + events.RemoveWorkloadEvent) + localState := resolver.LocalState{ + State: operation.State{ + Kind: operation.Continue, + Step: operation.Pending, + }, + } + remoteState := remotestate.Snapshot{ + WorkloadEvents: []string{ + events.AddWorkloadEvent(container.WorkloadEvent{ + Type: container.ChangeUpdatedEvent, + WorkloadName: "test", + NoticeID: "123", + NoticeType: "change-update", + NoticeKey: "42", + }, handler), + }, + } + opFactory := &mockOperations{} + op, err := containerResolver.NextOp(context.Background(), localState, remoteState, opFactory) + c.Assert(err, jc.ErrorIsNil) + c.Assert(op, gc.NotNil) + op = operation.Unwrap(op) + hookOp, ok := op.(*mockRunHookOp) + c.Assert(ok, jc.IsTrue) + c.Assert(hookOp.hookInfo, gc.DeepEquals, hook.Info{ + Kind: "pebble-change-updated", + WorkloadName: "test", + NoticeID: "123", + NoticeType: "change-update", + NoticeKey: "42", + }) +} diff --git a/internal/worker/uniter/hook/hook.go b/internal/worker/uniter/hook/hook.go index dea77b67fba..ab6784332ef 100644 --- a/internal/worker/uniter/hook/hook.go +++ b/internal/worker/uniter/hook/hook.go @@ -94,7 +94,7 @@ func (hi Info) Validate() error { return errors.Errorf("%q hook has a remote unit but no application", hi.Kind) } return nil - case hooks.PebbleCustomNotice: + case hooks.PebbleCustomNotice, hooks.PebbleChangeUpdated: if hi.WorkloadName == "" { return errors.Errorf("%q hook requires a workload name", hi.Kind) } diff --git a/internal/worker/uniter/hook/hook_test.go b/internal/worker/uniter/hook/hook_test.go index f8d12c3dcd5..f7e84e81455 100644 --- a/internal/worker/uniter/hook/hook_test.go +++ b/internal/worker/uniter/hook/hook_test.go @@ -52,6 +52,12 @@ var validateTests = []struct { }, { hook.Info{Kind: hooks.PebbleCustomNotice, WorkloadName: "test"}, `"pebble-custom-notice" hook requires a notice ID, type, and key`, + }, { + hook.Info{Kind: hooks.PebbleChangeUpdated}, + `"pebble-change-updated" hook requires a workload name`, + }, { + hook.Info{Kind: hooks.PebbleChangeUpdated, WorkloadName: "test"}, + `"pebble-change-updated" hook requires a notice ID, type, and key`, }, { hook.Info{Kind: hooks.PreSeriesUpgrade}, `"pre-series-upgrade" hook requires a target base`, diff --git a/internal/worker/uniter/pebblenotices.go b/internal/worker/uniter/pebblenotices.go index 5f242b1beeb..d8ce09c29ac 100644 --- a/internal/worker/uniter/pebblenotices.go +++ b/internal/worker/uniter/pebblenotices.go @@ -136,6 +136,8 @@ func (n *pebbleNoticer) processNotice(containerName string, notice *client.Notic switch notice.Type { case client.CustomNotice: eventType = container.CustomNoticeEvent + case client.ChangeUpdateNotice: + eventType = container.ChangeUpdatedEvent default: n.logger.Debugf("container %q: ignoring %s notice", containerName, notice.Type) return nil diff --git a/internal/worker/uniter/pebblenotices_test.go b/internal/worker/uniter/pebblenotices_test.go index 973e518795a..5b479cdb6b9 100644 --- a/internal/worker/uniter/pebblenotices_test.go +++ b/internal/worker/uniter/pebblenotices_test.go @@ -113,6 +113,25 @@ func (s *pebbleNoticerSuite) TestWaitNotices(c *gc.C) { }) } +func (s *pebbleNoticerSuite) TestChangeUpdate(c *gc.C) { + s.setUpWorker(c, []string{"c1"}) + defer workertest.CleanKill(c, s.worker) + + s.clients["c1"].AddNotice(c, &client.Notice{ + ID: "1", + Type: "change-update", + Key: "42", + LastRepeated: time.Now(), + }) + s.waitWorkloadEvent(c, container.WorkloadEvent{ + Type: container.ChangeUpdatedEvent, + WorkloadName: "c1", + NoticeID: "1", + NoticeType: "change-update", + NoticeKey: "42", + }) +} + func (s *pebbleNoticerSuite) TestWaitNoticesError(c *gc.C) { s.setUpWorker(c, []string{"c1"}) defer workertest.CleanKill(c, s.worker) diff --git a/internal/worker/uniter/runner/context/contextfactory.go b/internal/worker/uniter/runner/context/contextfactory.go index 5f4b6ba9602..4aa9297bde8 100644 --- a/internal/worker/uniter/runner/context/contextfactory.go +++ b/internal/worker/uniter/runner/context/contextfactory.go @@ -282,7 +282,7 @@ func (f *contextFactory) HookContext(stdCtx context.Context, hookInfo hook.Info) ctx.workloadName = hookInfo.WorkloadName hookName = fmt.Sprintf("%s-%s", hookInfo.WorkloadName, hookName) switch hookInfo.Kind { - case hooks.PebbleCustomNotice: + case hooks.PebbleCustomNotice, hooks.PebbleChangeUpdated: ctx.noticeID = hookInfo.NoticeID ctx.noticeType = hookInfo.NoticeType ctx.noticeKey = hookInfo.NoticeKey diff --git a/juju/testing/instance.go b/juju/testing/instance.go index ee665b335d7..8a520874a20 100644 --- a/juju/testing/instance.go +++ b/juju/testing/instance.go @@ -13,7 +13,6 @@ import ( "github.com/juju/juju/api" "github.com/juju/juju/core/arch" - corebase "github.com/juju/juju/core/base" "github.com/juju/juju/core/constraints" "github.com/juju/juju/core/instance" "github.com/juju/juju/core/model" @@ -187,11 +186,7 @@ func FillInStartInstanceParams(env environs.Environ, machineId string, isControl preferredBase := config.PreferredBase(env.Config()) if params.ImageMetadata == nil { - preferredSeries, err := corebase.GetSeriesFromBase(preferredBase) - if err != nil { - return errors.Trace(err) - } - vers, err := imagemetadata.ImageRelease(preferredSeries) + vers, err := imagemetadata.ImageRelease(preferredBase) if err != nil { return errors.Trace(err) } diff --git a/scripts/dqlite/scripts/dqlite/dqlite-build.sh b/scripts/dqlite/scripts/dqlite/dqlite-build.sh index f395a2cf244..a55c449ba65 100755 --- a/scripts/dqlite/scripts/dqlite/dqlite-build.sh +++ b/scripts/dqlite/scripts/dqlite/dqlite-build.sh @@ -50,7 +50,7 @@ build() { sudo ln -s /usr/include/linux /usr/local/musl/include/linux || true # Grab the queue.h file that does not ship with musl - sudo wget https://dev.midipix.org/compat/musl-compat/raw/main/f/include/sys/queue.h -O /usr/local/musl/include/sys/queue.h + sudo wget https://raw.githubusercontent.com/juju/musl-compat/main/include/sys/queue.h -O /usr/local/musl/include/sys/queue.h # Install compile dependencies for statically linking everything: # -------------------------------------------------------------- diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 08347da8e5a..dfe4536a37f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -98,7 +98,7 @@ parts: ln -s /usr/include/linux /usr/local/musl/include/linux musl-compat: - source: https://dev.midipix.org/compat/musl-compat.git + source: https://github.com/juju/musl-compat.git source-type: git source-depth: 1 plugin: nil diff --git a/testcharms/charms/pebble-notices/src/charm.py b/testcharms/charms/pebble-notices/src/charm.py index b2e505be047..901ff94ee0a 100755 --- a/testcharms/charms/pebble-notices/src/charm.py +++ b/testcharms/charms/pebble-notices/src/charm.py @@ -16,6 +16,24 @@ def __init__(self, *args): self.framework.observe(self.on["redis"].pebble_ready, self._on_pebble_ready) self.framework.observe(self.on["redis"].pebble_custom_notice, self._on_custom_notice) + # TODO(benhoyt): update to use pebble_change_updated once ops supports that: + # https://github.com/canonical/operator/pull/1170 + import os + import pathlib + + dispatch_path = pathlib.Path(os.environ.get("JUJU_DISPATCH_PATH", "")) + event_name = dispatch_path.name.replace("-", "_") + logger.info(f"__init__: path={dispatch_path} event={event_name}") + if event_name == "redis_pebble_change_updated": + event = ops.PebbleNoticeEvent( + None, + self.unit.get_container(os.environ["JUJU_WORKLOAD_NAME"]), + os.environ["JUJU_NOTICE_ID"], + os.environ["JUJU_NOTICE_TYPE"], + os.environ["JUJU_NOTICE_KEY"], + ) + self._on_change_updated(event) + def _on_pebble_ready(self, event): self.unit.status = ops.ActiveStatus() @@ -28,6 +46,19 @@ def _on_custom_notice(self, event): # Don't include the (arbitrary) ID in the status message self.unit.status = ops.MaintenanceStatus(f"notice type={notice_type} key={notice_key}") + def _on_change_updated(self, event): + notice_id = event.notice.id + notice_type = ( + event.notice.type if isinstance(event.notice.type, str) else event.notice.type.value + ) + notice_key = event.notice.key + logger.info(f"_on_change_updated: id={notice_id} type={notice_type} key={notice_key}") + + change = event.workload.pebble.get_change(notice_key) + self.unit.status = ops.MaintenanceStatus( + f"notice type={notice_type} kind={change.kind} status={change.status}" + ) + if __name__ == "__main__": ops.main(PebbleNoticesCharm) diff --git a/testcharms/charms/pebble-notices/tests/unit/test_charm.py b/testcharms/charms/pebble-notices/tests/unit/test_charm.py index b7bbc39bfdc..9904dc56c22 100644 --- a/testcharms/charms/pebble-notices/tests/unit/test_charm.py +++ b/testcharms/charms/pebble-notices/tests/unit/test_charm.py @@ -1,8 +1,10 @@ import unittest +import unittest.mock import ops import ops.testing from charm import PebbleNoticesCharm +from ops import pebble class TestCharm(unittest.TestCase): @@ -10,6 +12,7 @@ def setUp(self): self.harness = ops.testing.Harness(PebbleNoticesCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() + self._next_notice_id = 1 def test_pebble_ready(self): self.harness.container_pebble_ready("redis") @@ -27,3 +30,68 @@ def test_custom_notice(self): self.harness.model.unit.status, ops.MaintenanceStatus("notice type=custom key=ubuntu.com/bar/buzz"), ) + + @unittest.mock.patch("ops.testing._TestingPebbleClient.get_change") + def test_change_updated(self, mock_get_change): + # TODO(benhoyt): update to use pebble_change_updated once ops supports that: + # https://github.com/canonical/operator/pull/1170 + + import os + + os.environ["JUJU_DISPATCH_PATH"] = "hooks/redis-pebble-change-updated" + self.addCleanup(os.environ.__delitem__, "JUJU_DISPATCH_PATH") + self.addCleanup(os.environ.__delitem__, "JUJU_NOTICE_ID") + self.addCleanup(os.environ.__delitem__, "JUJU_NOTICE_TYPE") + self.addCleanup(os.environ.__delitem__, "JUJU_NOTICE_KEY") + + mock_get_change.return_value = pebble.Change.from_dict( + { + "id": "1", + "kind": "exec", + "summary": "", + "status": "Doing", + "ready": False, + "spawn-time": "2021-01-28T14:37:02.247202105+13:00", + } + ) + self._pebble_notify_change_updated("redis", "123") + self.assertEqual( + self.harness.model.unit.status, + ops.MaintenanceStatus("notice type=change-update kind=exec status=Doing"), + ) + mock_get_change.assert_called_once_with("123") + + mock_get_change.reset_mock() + mock_get_change.return_value = pebble.Change.from_dict( + { + "id": "2", + "kind": "changeroo", + "summary": "", + "status": "Done", + "ready": True, + "spawn-time": "2024-01-28T14:37:02.247202105+13:00", + "ready-time": "2024-01-28T14:37:04.291517768+13:00", + } + ) + self._pebble_notify_change_updated("redis", "42") + self.assertEqual( + self.harness.model.unit.status, + ops.MaintenanceStatus("notice type=change-update kind=changeroo status=Done"), + ) + mock_get_change.assert_called_once_with("42") + + def _pebble_notify_change_updated(self, container_name, notice_key): + import os + + os.environ["JUJU_NOTICE_ID"] = notice_id = str(self._next_notice_id) + self._next_notice_id += 1 + os.environ["JUJU_NOTICE_TYPE"] = notice_type = "change-update" + os.environ["JUJU_NOTICE_KEY"] = notice_key + event = ops.PebbleNoticeEvent( + None, + self.harness.model.unit.get_container(container_name), + notice_id, + notice_type, + notice_key, + ) + self.harness.charm._on_change_updated(event) diff --git a/tests/includes/wait-for.sh b/tests/includes/wait-for.sh index 6a6ac15ecb0..7fef3fc846d 100644 --- a/tests/includes/wait-for.sh +++ b/tests/includes/wait-for.sh @@ -351,7 +351,7 @@ wait_for_aws_ingress_cidrs_for_port_range() { secgrp_list=$(aws ec2 describe-security-groups --filters Name=ip-permission.from-port,Values=${from_port} Name=ip-permission.to-port,Values=${to_port}) # print the security group rules # shellcheck disable=SC2086 - got_cidrs=$(echo ${secgrp_list} | jq -r ".SecurityGroups[0].IpPermissions | .[] | select(.FromPort == ${from_port} and .ToPort == ${to_port}) | .Ip${ipV6Suffix}Ranges | .[] | .CidrIp${ipV6Suffix}" | sort | paste -sd, -) + got_cidrs=$(echo ${secgrp_list} | jq -r ".SecurityGroups[0].IpPermissions // [] | .[] | select(.FromPort == ${from_port} and .ToPort == ${to_port}) | .Ip${ipV6Suffix}Ranges // [] | .[] | .CidrIp${ipV6Suffix}" | sort | paste -sd, -) attempt=0 # shellcheck disable=SC2046,SC2143 @@ -360,7 +360,7 @@ wait_for_aws_ingress_cidrs_for_port_range() { # shellcheck disable=SC2086 secgrp_list=$(aws ec2 describe-security-groups --filters Name=ip-permission.from-port,Values=${from_port} Name=ip-permission.to-port,Values=${to_port}) # shellcheck disable=SC2086 - got_cidrs=$(echo ${secgrp_list} | jq -r ".SecurityGroups[0].IpPermissions | .[] | select(.FromPort == ${from_port} and .ToPort == ${to_port}) | .Ip${ipV6Suffix}Ranges | .[] | .CidrIp${ipV6Suffix}" | sort | paste -sd, -) + got_cidrs=$(echo ${secgrp_list} | jq -r ".SecurityGroups[0].IpPermissions // [] | .[] | select(.FromPort == ${from_port} and .ToPort == ${to_port}) | .Ip${ipV6Suffix}Ranges // [] | .[] | .CidrIp${ipV6Suffix}" | sort | paste -sd, -) sleep "${SHORT_TIMEOUT}" if [ "$got_cidrs" == "$exp_cidrs" ]; then diff --git a/tests/suites/sidecar/sidecar.sh b/tests/suites/sidecar/sidecar.sh index 65851b4ca5a..26db62d256f 100644 --- a/tests/suites/sidecar/sidecar.sh +++ b/tests/suites/sidecar/sidecar.sh @@ -89,3 +89,24 @@ test_pebble_notices() { # Clean up model destroy_model "${model_name}" } + +test_pebble_change_updated() { + echo + + # Ensure that a valid Juju controller exists + model_name="controller-model-sidecar" + file="${TEST_DIR}/test-${model_name}.log" + ensure "${model_name}" "${file}" + + # Deploy Pebble Notices test application + juju deploy juju-qa-pebble-notices + wait_for "active" '.applications["juju-qa-pebble-notices"].units["juju-qa-pebble-notices/0"]["workload-status"].current' + + # Check that charm is responding correctly to a change-update notice + juju ssh --container redis juju-qa-pebble-notices/0 /charm/bin/pebble exec -- echo foo + wait_for "maintenance" '.applications["juju-qa-pebble-notices"].units["juju-qa-pebble-notices/0"]["workload-status"].current' + wait_for "notice type=change-update kind=exec status=Done" '.applications["juju-qa-pebble-notices"].units["juju-qa-pebble-notices/0"]["workload-status"].message' + + # Clean up model + destroy_model "${model_name}" +} diff --git a/tests/suites/sidecar/task.sh b/tests/suites/sidecar/task.sh index 581ec57ba37..4812036a953 100644 --- a/tests/suites/sidecar/task.sh +++ b/tests/suites/sidecar/task.sh @@ -11,6 +11,7 @@ test_sidecar() { test_deploy_and_remove_application test_deploy_and_force_remove_application test_pebble_notices + test_pebble_change_updated ;; *) echo "==> TEST SKIPPED: sidecar charm tests, not a k8s provider"