diff --git a/api/apiclient.go b/api/apiclient.go index fc1ead8b1a5..a8b923a4992 100644 --- a/api/apiclient.go +++ b/api/apiclient.go @@ -1227,35 +1227,12 @@ func isX509Error(err error) bool { // object id, and the specific RPC method. It marshalls the Arguments, and will // unmarshall the result into the response object that is supplied. func (s *state) APICall(facade string, vers int, id, method string, args, response interface{}) error { - err := s.client.Call(rpc.Request{ + return s.client.Call(rpc.Request{ Type: facade, Version: vers, Id: id, Action: method, }, args, response) - if err == nil { - return nil - } - code := params.ErrCode(err) - if code != params.CodeIncompatibleClient { - return errors.Trace(err) - } - // Default to major version 2 for older servers. - serverMajorVersion := 2 - err = errors.Cause(err) - apiErr, ok := err.(*rpc.RequestError) - if ok { - if serverVersion, ok := apiErr.Info["server-version"]; ok { - serverVers, err := version.Parse(fmt.Sprintf("%v", serverVersion)) - if err == nil { - serverMajorVersion = serverVers.Major - } - } - } - logger.Debugf("%v.%v API call not supported", facade, method) - return errors.NewNotSupported(nil, fmt.Sprintf( - "juju client with version %d.%d used with a controller having major version %d not supported\nre-install your juju client to match the version running on the controller", - jujuversion.Current.Major, jujuversion.Current.Minor, serverMajorVersion)) } func (s *state) Close() error { diff --git a/api/apiclient_test.go b/api/apiclient_test.go index 7fdac2c207b..e04d6a85d99 100644 --- a/api/apiclient_test.go +++ b/api/apiclient_test.go @@ -1187,24 +1187,6 @@ func (s *apiclientSuite) TestLoginCapturesCLIArgs(c *gc.C) { c.Assert(request.CLIArgs, gc.Equals, `this is "the test" command`) } -func (s *apiclientSuite) TestLoginIncompatibleClient(c *gc.C) { - clock := &fakeClock{} - conn := api.NewTestingState(api.TestingStateParams{ - RPCConnection: newRPCConnection(&rpc.RequestError{ - Code: "incompatible client", - Info: map[string]interface{}{"server-version": "99.0.0"}, - }), - Clock: clock, - }) - - err := conn.APICall("facade", 1, "id", "method", nil, nil) - c.Check(clock.waits, gc.HasLen, 0) - c.Assert(err, gc.ErrorMatches, fmt.Sprintf( - "juju client with version %d.%d used with a controller having major version %d not supported\\n.*", - jujuversion.Current.Major, jujuversion.Current.Minor, 99, - )) -} - type clientDNSNameSuite struct { jjtesting.JujuConnSuite } diff --git a/apiserver/admin.go b/apiserver/admin.go index 71623b95ced..0635a220e14 100644 --- a/apiserver/admin.go +++ b/apiserver/admin.go @@ -13,7 +13,6 @@ import ( "github.com/juju/errors" "github.com/juju/names/v4" "github.com/juju/rpcreflect" - "github.com/juju/version/v2" "github.com/juju/juju/api" "github.com/juju/juju/apiserver/common" @@ -136,19 +135,11 @@ func (a *admin) login(ctx context.Context, req params.LoginRequest, loginVersion return fail, errors.Trace(err) } - // Default client version to 2 since older 2.x clients - // don't send this field. - loginClientVersion := version.Number{Major: 2} - if clientVersion, err := version.Parse(req.ClientVersion); err == nil { - loginClientVersion = clientVersion - } - apiRoot, err = restrictAPIRoot( a.srv, apiRoot, a.root.model, *authResult, - loginClientVersion, ) if err != nil { return fail, errors.Trace(err) diff --git a/apiserver/errors/errors.go b/apiserver/errors/errors.go index 18d59cd8b95..2dbe954d950 100644 --- a/apiserver/errors/errors.go +++ b/apiserver/errors/errors.go @@ -150,7 +150,6 @@ func ServerError(err error) *params.Error { var ( dischargeRequiredError *DischargeRequiredError - incompatibleClientError *params.IncompatibleClientError notLeaderError *NotLeaderError redirectError *RedirectError upgradeSeriesValidationError *UpgradeSeriesValidationError @@ -242,9 +241,6 @@ func ServerError(err error) *params.Error { code = params.CodeNotYetAvailable case errors.Is(err, ErrTryAgain): code = params.CodeTryAgain - case errors.As(err, &incompatibleClientError): - code = params.CodeIncompatibleClient - info = incompatibleClientError.AsMap() case errors.As(err, ¬LeaderError): code = params.CodeNotLeader info = notLeaderError.AsMap() diff --git a/apiserver/errors/errors_test.go b/apiserver/errors/errors_test.go index 9563fc4d857..9eb23bd1422 100644 --- a/apiserver/errors/errors_test.go +++ b/apiserver/errors/errors_test.go @@ -23,7 +23,6 @@ import ( "github.com/juju/juju/rpc/params" stateerrors "github.com/juju/juju/state/errors" "github.com/juju/juju/testing" - jujuversion "github.com/juju/juju/version" ) type errorsSuite struct { @@ -239,25 +238,6 @@ var errorTransformTests = []struct { code: params.CodeNotYetAvailable, status: http.StatusConflict, helperFunc: params.IsCodeNotYetAvailable, -}, { - err: ¶ms.IncompatibleClientError{ - ServerVersion: jujuversion.Current, - }, - code: params.CodeIncompatibleClient, - status: http.StatusInternalServerError, - helperFunc: func(err error) bool { - err1, ok := err.(*params.Error) - err2 := ¶ms.IncompatibleClientError{ - ServerVersion: jujuversion.Current, - } - if !ok || err1.Info == nil || !reflect.DeepEqual(err1.Info, err2.AsMap()) { - return false - } - return true - }, - targetTester: func(e error) bool { - return errors.HasType[*params.IncompatibleClientError](e) - }, }, { err: apiservererrors.NewNotLeaderError("1.1.1.1", "1"), code: params.CodeNotLeader, @@ -373,8 +353,7 @@ func (s *errorsSuite) TestErrorTransform(c *gc.C) { params.CodeMachineHasAttachedStorage, params.CodeDischargeRequired, params.CodeModelNotFound, - params.CodeRedirect, - params.CodeIncompatibleClient: + params.CodeRedirect: continue case params.CodeOperationBlocked: // ServerError doesn't actually have a case for this code. diff --git a/apiserver/export_test.go b/apiserver/export_test.go index db725ed8ee0..e1e4b213dac 100644 --- a/apiserver/export_test.go +++ b/apiserver/export_test.go @@ -9,7 +9,6 @@ import ( "github.com/juju/clock" "github.com/juju/names/v4" jc "github.com/juju/testing/checkers" - "github.com/juju/version/v2" gc "gopkg.in/check.v1" "github.com/juju/juju/apiserver/common" @@ -146,20 +145,6 @@ func TestingRestrictedRoot(check func(string, string) error) rpc.Root { return restrictRoot(r, check) } -// TestingAboutToRestoreRoot returns a limited root which allows -// methods as per when a restore is about to happen. -func TestingAboutToRestoreRoot() rpc.Root { - r := TestingAPIRoot(AllFacades()) - return restrictRoot(r, aboutToRestoreMethodsOnly) -} - -// TestingUpgradeOrMigrationOnlyRoot returns a restricted srvRoot -// as if called from a newer client. -func TestingUpgradeOrMigrationOnlyRoot(userLogin bool, clientVersion version.Number) rpc.Root { - r := TestingAPIRoot(AllFacades()) - return restrictRoot(r, checkClientVersion(userLogin, clientVersion)) -} - // PatchGetMigrationBackend overrides the getMigrationBackend function // to support testing. func PatchGetMigrationBackend(p Patcher, ctrlSt controllerBackend, st migrationBackend) { diff --git a/apiserver/facades/client/application/application.go b/apiserver/facades/client/application/application.go index 937e6973a68..38105a727fc 100644 --- a/apiserver/facades/client/application/application.go +++ b/apiserver/facades/client/application/application.go @@ -417,16 +417,19 @@ func (api *APIBase) Deploy(args params.ApplicationsDeploy) (params.ErrorResults, } for i, arg := range args.Applications { - err := deployApplication( - api.backend, - api.model, - api.stateCharm, - arg, - api.deployApplicationFunc, - api.storagePoolManager, - api.registry, - api.caasBroker, - ) + var err error + if arg, err = compatibilityApplicationDeployArgs(arg); err == nil { + err = deployApplication( + api.backend, + api.model, + api.stateCharm, + arg, + api.deployApplicationFunc, + api.storagePoolManager, + api.registry, + api.caasBroker, + ) + } result.Results[i].Error = apiservererrors.ServerError(err) if err != nil && len(arg.Resources) != 0 { @@ -447,6 +450,32 @@ func (api *APIBase) Deploy(args params.ApplicationsDeploy) (params.ErrorResults, return result, nil } +// compatibilityApplicationDeployArgs ensures that Deploy calls from +// a juju 3.x client will work against a juju 2.9.x controller. In +// juju 3.x, params.ApplicationsDeploy was changed to remove series +// however, the facade version was not changed, nor was the name of +// the params.ApplicationsDeploy changed. Thus it appears you can use +// a juju 3.x client to deploy from a juju 2.9 controller, however it +// fails because the series was not found. Make those corrections here. +func compatibilityApplicationDeployArgs(arg params.ApplicationDeploy) (params.ApplicationDeploy, error) { + origin := arg.CharmOrigin + if origin.Series != "" { + return arg, nil + } + originSeries, err := series.GetSeriesFromChannel(origin.Base.Name, origin.Base.Channel) + if err != nil { + return arg, err + } + curl, err := charm.ParseURL(arg.CharmURL) + if err != nil { + return arg, err + } + arg.CharmURL = curl.WithSeries(originSeries).String() + origin.Series = originSeries + arg.Series = originSeries + return arg, nil +} + func applicationConfigSchema(modelType state.ModelType) (environschema.Fields, schema.Defaults, error) { if modelType != state.ModelTypeCAAS { return trustFields, trustDefaults, nil diff --git a/apiserver/facades/client/application/application_test.go b/apiserver/facades/client/application/application_test.go index 3eda04aa973..b92653ebc31 100644 --- a/apiserver/facades/client/application/application_test.go +++ b/apiserver/facades/client/application/application_test.go @@ -770,9 +770,14 @@ func (s *applicationSuite) testClientApplicationsDeployWithBindings(c *gc.C, end var cons constraints.Value args := params.ApplicationDeploy{ - ApplicationName: "application", - CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + ApplicationName: "application", + CharmURL: curl.String(), + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, NumUnits: 1, Constraints: cons, EndpointBindings: endpointBindings, @@ -986,8 +991,13 @@ func (s *applicationSuite) TestApplicationSetCharm(c *gc.C) { } results, err := s.applicationAPI.Deploy(params.ApplicationsDeploy{ Applications: []params.ApplicationDeploy{{ - CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmURL: curl.String(), + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ApplicationName: "application", NumUnits: numUnits, }}}) @@ -1009,7 +1019,12 @@ func (s *applicationSuite) TestApplicationSetCharm(c *gc.C) { err = s.applicationAPI.SetCharm(params.ApplicationSetCharm{ ApplicationName: "application", CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, }) c.Assert(err, jc.ErrorIsNil) @@ -1035,8 +1050,13 @@ func (s *applicationSuite) setupApplicationSetCharm(c *gc.C) { c.Assert(err, jc.ErrorIsNil) results, err := s.applicationAPI.Deploy(params.ApplicationsDeploy{ Applications: []params.ApplicationDeploy{{ - CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmURL: curl.String(), + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ApplicationName: "application", NumUnits: numUnits, }}}) @@ -1077,7 +1097,12 @@ func (s *applicationSuite) assertApplicationSetCharmBlocked(c *gc.C, msg string) err := s.applicationAPI.SetCharm(params.ApplicationSetCharm{ ApplicationName: "application", CharmURL: "cs:~who/quantal/wordpress-3", - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, }) s.AssertBlocked(c, err, msg) } @@ -1113,8 +1138,13 @@ func (s *applicationSuite) TestApplicationSetCharmForceUnits(c *gc.C) { } results, err := s.applicationAPI.Deploy(params.ApplicationsDeploy{ Applications: []params.ApplicationDeploy{{ - CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmURL: curl.String(), + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ApplicationName: "application", NumUnits: numUnits, }}}) @@ -1165,9 +1195,13 @@ func (s *applicationSuite) TestApplicationSetCharmInvalidApplication(c *gc.C) { err := s.applicationAPI.SetCharm(params.ApplicationSetCharm{ ApplicationName: "badapplication", CharmURL: "cs:quantal/wordpress-3", - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, - ForceSeries: true, - ForceUnits: true, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ForceSeries: true, + ForceUnits: true, }) c.Assert(err, gc.ErrorMatches, `application "badapplication" not found`) } @@ -1195,8 +1229,13 @@ func (s *applicationSuite) TestApplicationSetCharmLegacy(c *gc.C) { c.Assert(err, jc.ErrorIsNil) results, err := s.applicationAPI.Deploy(params.ApplicationsDeploy{ Applications: []params.ApplicationDeploy{{ - CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmURL: curl.String(), + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ApplicationName: "application", }}}) c.Assert(err, jc.ErrorIsNil) @@ -1213,8 +1252,12 @@ func (s *applicationSuite) TestApplicationSetCharmLegacy(c *gc.C) { err = s.applicationAPI.SetCharm(params.ApplicationSetCharm{ ApplicationName: "application", CharmURL: curl.String(), - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, - ForceSeries: true, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ForceSeries: true, }) c.Assert(err, gc.ErrorMatches, `cannot upgrade application "application" to charm "cs:~who/trusty/dummy-1": cannot change an application's series`) } @@ -1660,8 +1703,13 @@ func (s *applicationSuite) TestApplicationDeployToMachineWithInvalidLXDProfileAn func (s *applicationSuite) TestApplicationDeployToMachineNotFound(c *gc.C) { results, err := s.applicationAPI.Deploy(params.ApplicationsDeploy{ Applications: []params.ApplicationDeploy{{ - CharmURL: "cs:quantal/application-name-1", - CharmOrigin: ¶ms.CharmOrigin{Source: "charm-store", OS: "ubuntu", Channel: "12.10"}, + CharmURL: "cs:quantal/application-name-1", + CharmOrigin: ¶ms.CharmOrigin{ + Source: "charm-store", + Base: params.Base{ + Name: "ubuntu", Channel: "12.10", + }, + }, ApplicationName: "application-name", NumUnits: 1, Placement: []*instance.Placement{instance.MustParsePlacement("42")}, diff --git a/apiserver/facades/client/application/application_unit_test.go b/apiserver/facades/client/application/application_unit_test.go index cd10d9e1e09..0cea1da5a78 100644 --- a/apiserver/facades/client/application/application_unit_test.go +++ b/apiserver/facades/client/application/application_unit_test.go @@ -1385,8 +1385,14 @@ func (s *ApplicationSuite) TestDeployCAASBlockStorageRejected(c *gc.C) { Applications: []params.ApplicationDeploy{{ ApplicationName: "foo", CharmURL: "local:foo-0", - CharmOrigin: ¶ms.CharmOrigin{Source: "local"}, - NumUnits: 1, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "local", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }}, + NumUnits: 1, }}, } result, err := s.api.Deploy(args) @@ -1412,8 +1418,15 @@ func (s *ApplicationSuite) TestDeployCAASModelNoOperatorStorage(c *gc.C) { Applications: []params.ApplicationDeploy{{ ApplicationName: "foo", CharmURL: "local:foo-0", - CharmOrigin: ¶ms.CharmOrigin{Source: "local"}, - NumUnits: 1, + CharmOrigin: ¶ms.CharmOrigin{ + Source: "local", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + }, + NumUnits: 1, }}, } result, err := s.api.Deploy(args) diff --git a/apiserver/facades/client/charms/client.go b/apiserver/facades/client/charms/client.go index a2a893125ea..1f15f0be407 100644 --- a/apiserver/facades/client/charms/client.go +++ b/apiserver/facades/client/charms/client.go @@ -288,7 +288,38 @@ func (a *API) AddCharmWithAuthorization(args params.AddCharmWithAuth) (params.Ch return a.addCharmWithAuthorization(args) } +// compatibilityAddCharmWithAuthArg ensures that AddCharm calls from +// a juju 3.x client will work against a juju 2.9.x controller. In +// juju 3.x, params.AddCharmWithAuth was changed to remove series +// however, the facade version was not changed, nor was the name of +// the params.AddCharmWithAuth changed. Thus it appears you can use +// a juju 3.x client to deploy from a juju 2.9 controller, however it +// fails because the series was not found. Make those corrections here. +func compatibilityAddCharmWithAuthArg(arg params.AddCharmWithAuth) (params.AddCharmWithAuth, error) { + origin := arg.Origin + if origin.Series != "" { + return arg, nil + } + originSeries, err := series.GetSeriesFromChannel(origin.Base.Name, origin.Base.Channel) + if err != nil { + return arg, err + } + curl, err := charm.ParseURL(arg.URL) + if err != nil { + return arg, err + } + arg.URL = curl.WithSeries(originSeries).String() + arg.Origin.Series = originSeries + arg.Series = originSeries + return arg, nil +} + func (a *API) addCharmWithAuthorization(args params.AddCharmWithAuth) (params.CharmOriginResult, error) { + var err error + args, err = compatibilityAddCharmWithAuthArg(args) + if err != nil { + return params.CharmOriginResult{}, errors.Annotatef(err, "compatibility updates of origin failed") + } if args.Origin.Source != "charm-hub" && args.Origin.Source != "charm-store" { return params.CharmOriginResult{}, errors.Errorf("unknown schema for charm URL %q", args.URL) } @@ -520,7 +551,6 @@ func (a *API) resolveOneCharm(arg params.ResolveCharmWithChannel, mac *macaroon. return result } result.URL = resultURL.String() - apiOrigin, err := convertOrigin(origin) if err != nil { result.Error = apiservererrors.ServerError(err) diff --git a/apiserver/facades/client/charms/client_test.go b/apiserver/facades/client/charms/client_test.go index 528224f2094..ef2d6af85b7 100644 --- a/apiserver/facades/client/charms/client_test.go +++ b/apiserver/facades/client/charms/client_test.go @@ -289,18 +289,22 @@ func (s *charmsMockSuite) TestAddCharmWithLocalSource(c *gc.C) { URL: curl.String(), Origin: params.CharmOrigin{ Source: "local", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04", + }, }, Force: false, } _, err = api.AddCharm(args) - c.Assert(err, gc.ErrorMatches, `unknown schema for charm URL "local:testme"`) + c.Assert(err, gc.ErrorMatches, `unknown schema for charm URL "local:jammy/testme"`) } func (s *charmsMockSuite) TestAddCharm(c *gc.C) { defer s.setupMocks(c).Finish() s.state.EXPECT().ControllerConfig().Return(controller.Config{}, nil) - curl, err := charm.ParseURL("cs:testme-8") + curl, err := charm.ParseURL("cs:jammy/testme-8") c.Assert(err, jc.ErrorIsNil) requestedOrigin := corecharm.Origin{ @@ -308,12 +312,22 @@ func (s *charmsMockSuite) TestAddCharm(c *gc.C) { Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } resolvedOrigin := corecharm.Origin{ Source: "charm-store", Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } s.downloader.EXPECT().DownloadAndStore(curl, requestedOrigin, nil, false).Return(resolvedOrigin, nil) @@ -323,8 +337,16 @@ func (s *charmsMockSuite) TestAddCharm(c *gc.C) { args := params.AddCharmWithOrigin{ URL: curl.String(), Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, Force: false, } @@ -332,8 +354,16 @@ func (s *charmsMockSuite) TestAddCharm(c *gc.C) { c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, params.CharmOriginResult{ Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, }) } @@ -342,7 +372,7 @@ func (s *charmsMockSuite) TestAddCharmWithAuthorization(c *gc.C) { defer s.setupMocks(c).Finish() s.state.EXPECT().ControllerConfig().Return(controller.Config{}, nil) - curl, err := charm.ParseURL("cs:testme-8") + curl, err := charm.ParseURL("cs:jammy/testme-8") c.Assert(err, jc.ErrorIsNil) requestedOrigin := corecharm.Origin{ @@ -350,12 +380,22 @@ func (s *charmsMockSuite) TestAddCharmWithAuthorization(c *gc.C) { Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } resolvedOrigin := corecharm.Origin{ Source: "charm-store", Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } mac, err := macaroon.New(nil, []byte("id"), "", macaroon.LatestVersion) @@ -369,8 +409,16 @@ func (s *charmsMockSuite) TestAddCharmWithAuthorization(c *gc.C) { args := params.AddCharmWithAuth{ URL: curl.String(), Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, Force: false, CharmStoreMacaroon: mac, @@ -379,8 +427,16 @@ func (s *charmsMockSuite) TestAddCharmWithAuthorization(c *gc.C) { c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, params.CharmOriginResult{ Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, }) } @@ -388,7 +444,7 @@ func (s *charmsMockSuite) TestAddCharmWithAuthorization(c *gc.C) { func (s *charmsMockSuite) TestQueueAsyncCharmDownload(c *gc.C) { defer s.setupMocks(c).Finish() - curl, err := charm.ParseURL("cs:testme-8") + curl, err := charm.ParseURL("cs:jammy/testme-8") c.Assert(err, jc.ErrorIsNil) requestedOrigin := corecharm.Origin{ @@ -396,12 +452,22 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownload(c *gc.C) { Channel: &charm.Channel{ Risk: "edge", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } resolvedOrigin := corecharm.Origin{ Source: "charm-store", Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } s.state.EXPECT().ControllerConfig().Return(controller.Config{ @@ -442,8 +508,16 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownload(c *gc.C) { args := params.AddCharmWithOrigin{ URL: curl.String(), Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "edge", + Source: "charm-store", + Risk: "edge", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, Force: false, } @@ -451,8 +525,16 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownload(c *gc.C) { c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, params.CharmOriginResult{ Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, }) } @@ -460,7 +542,7 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownload(c *gc.C) { func (s *charmsMockSuite) TestQueueAsyncCharmDownloadResolvesAgainOriginForAlreadyDownloadedCharm(c *gc.C) { defer s.setupMocks(c).Finish() - curl, err := charm.ParseURL("cs:testme-8") + curl, err := charm.ParseURL("cs:jammy/testme-8") c.Assert(err, jc.ErrorIsNil) resURL, err := url.Parse(curl.String()) c.Assert(err, jc.ErrorIsNil) @@ -470,6 +552,11 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownloadResolvesAgainOriginForAlrea Channel: &charm.Channel{ Risk: "stable", }, + Platform: corecharm.Platform{ + Architecture: "amd64", + OS: "ubuntu", + Channel: "22.04", + }, } s.state.EXPECT().ControllerConfig().Return(controller.Config{ @@ -484,8 +571,16 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownloadResolvesAgainOriginForAlrea args := params.AddCharmWithOrigin{ URL: curl.String(), Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "edge", + Source: "charm-store", + Risk: "edge", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, Force: false, } @@ -493,8 +588,16 @@ func (s *charmsMockSuite) TestQueueAsyncCharmDownloadResolvesAgainOriginForAlrea c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, params.CharmOriginResult{ Origin: params.CharmOrigin{ - Source: "charm-store", - Risk: "stable", + Source: "charm-store", + Risk: "stable", + Architecture: "amd64", + Base: params.Base{ + Name: "ubuntu", + Channel: "22.04/stable", + }, + Series: "jammy", + OS: "ubuntu", + Channel: "22.04", }, }, gc.Commentf("expected to get back the origin recorded by the application")) } diff --git a/apiserver/facades/client/charms/conversions.go b/apiserver/facades/client/charms/conversions.go index bc14686f5a1..4f1245e9bbd 100644 --- a/apiserver/facades/client/charms/conversions.go +++ b/apiserver/facades/client/charms/conversions.go @@ -33,6 +33,14 @@ func convertOrigin(origin corecharm.Origin) (params.CharmOrigin, error) { chSeries = origin.Platform.Channel } } + var base series.Base + if origin.Platform.Channel != "" { + var err error + base, err = series.ParseBase(origin.Platform.OS, origin.Platform.Channel) + if err != nil { + return params.CharmOrigin{}, errors.Trace(err) + } + } return params.CharmOrigin{ Source: string(origin.Source), Type: origin.Type, @@ -47,6 +55,7 @@ func convertOrigin(origin corecharm.Origin) (params.CharmOrigin, error) { Channel: origin.Platform.Channel, // TODO(juju3) - remove series Series: chSeries, + Base: params.Base{Name: base.Name, Channel: base.Channel.String()}, InstanceKey: origin.InstanceKey, }, nil } diff --git a/apiserver/facades/client/machinemanager/machinemanager.go b/apiserver/facades/client/machinemanager/machinemanager.go index ec9892c5e7e..2454c1f008b 100644 --- a/apiserver/facades/client/machinemanager/machinemanager.go +++ b/apiserver/facades/client/machinemanager/machinemanager.go @@ -229,6 +229,25 @@ func (mm *MachineManagerAPIV7) AddMachines(args params.AddMachines) (params.AddM return mm.MachineManagerAPI.AddMachines(args) } +// compatibilityMachineParams ensures that AddMachine called from a juju 3.x +// client will work against a juju 2.9.x controller. In juju 3.x, +// params.AddMachineParams was changed to remove series however, the facade +// version was not changed, nor was the name of the params.AddMachineParams +// changed. Thus it appears you can use a juju 3.x client to deploy from a +// juju 2.9 controller, which then fails because the series was not found. +// Make those corrections here. +func compatibilityMachineParams(arg params.AddMachineParams) (params.AddMachineParams, error) { + if arg.Base == nil { + return arg, nil + } + machineSeries, err := series.GetSeriesFromChannel(arg.Base.Name, arg.Base.Channel) + if err != nil { + return arg, err + } + arg.Series = machineSeries + return arg, nil +} + // AddMachines adds new machines with the supplied parameters. // The args will contain Base info. func (mm *MachineManagerAPI) AddMachines(args params.AddMachines) (params.AddMachinesResults, error) { @@ -242,6 +261,12 @@ func (mm *MachineManagerAPI) AddMachines(args params.AddMachines) (params.AddMac return results, errors.Trace(err) } for i, p := range args.MachineParams { + var err error + p, err = compatibilityMachineParams(p) + if err != nil { + results.Machines[i].Error = apiservererrors.ServerError(errors.Annotatef(err, "compatibility updates of arg failed")) + continue + } m, err := mm.addOneMachine(p) results.Machines[i].Error = apiservererrors.ServerError(err) if err == nil { diff --git a/apiserver/restrict_newer_client.go b/apiserver/restrict_newer_client.go deleted file mode 100644 index 0ccedbd80b7..00000000000 --- a/apiserver/restrict_newer_client.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2020 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package apiserver - -import ( - "github.com/juju/collections/set" - "github.com/juju/version/v2" - - "github.com/juju/juju/rpc/params" - "github.com/juju/juju/upgrades/upgradevalidation" - jujuversion "github.com/juju/juju/version" -) - -func checkClientVersion(userLogin bool, callerVersion version.Number) func(facadeName, methodName string) error { - return func(facadeName, methodName string) error { - serverVersion := jujuversion.Current - incompatibleClientError := ¶ms.IncompatibleClientError{ - ServerVersion: serverVersion, - } - // If client or server versions are more than one major version apart, - // reject the call immediately. - if callerVersion.Major < serverVersion.Major-1 || callerVersion.Major > serverVersion.Major+1 { - return incompatibleClientError - } - // Connection pings always need to be allowed. - if facadeName == "Pinger" && methodName == "Ping" { - return nil - } - - if !userLogin { - // Only recent older agents can make api calls. - if minAgentVersion, ok := upgradevalidation.MinAgentVersions[serverVersion.Major]; !ok || callerVersion.Compare(minAgentVersion) < 0 { - logger.Warningf("rejected agent api all %v.%v for agent version %v", facadeName, methodName, callerVersion) - return incompatibleClientError - } - return nil - } - - // Calls to manage the migration of the target controller - // always need to be allowed. - if facadeName == "MigrationTarget" { - return nil - } - // Some calls like status we will support always. - if isMethodAllowedForDifferentClients(facadeName, methodName) { - return nil - } - - // The migration worker makes calls masquerading as a user - // so we need to treat those separately. - if isMethodAllowedForMigrate(facadeName, methodName) { - return nil - } - - // Older clients can only connect to the next 0 version minor release, - // and then only if the client version is recent enough. - olderClient := callerVersion.Major < serverVersion.Major - if olderClient { - if serverVersion.Minor > 0 { - return incompatibleClientError - } - // Check whitelisted client versions. - if minClientVersion, ok := upgradevalidation.MinClientVersions[serverVersion.Major]; ok && callerVersion.Compare(minClientVersion) >= 0 { - return nil - } - return incompatibleClientError - } - - // Some calls are needed for upgrades. - if isMethodAllowedForUpgrade(facadeName, methodName) { - return nil - } - - // Very new clients are rejected outright if not otherwise whitelisted above. - veryNewCaller := callerVersion.Major > serverVersion.Major && callerVersion.Minor != 0 - if veryNewCaller { - return incompatibleClientError - } - - // Newer clients with a 0 minor version can only connect to a server if it - // is recent enough. - if minServerVersion, ok := upgradevalidation.MinClientVersions[callerVersion.Major]; ok && serverVersion.Compare(minServerVersion) >= 0 { - return nil - } - - return incompatibleClientError - } -} - -func isMethodAllowedForDifferentClients(facadeName, methodName string) bool { - methods, ok := allowedDifferentClientMethods[facadeName] - if !ok { - return false - } - return methods.Contains(methodName) -} - -func isMethodAllowedForUpgrade(facadeName, methodName string) bool { - upgradeOK := false - upgradeMethods, ok := allowedMethodsForUpgrade[facadeName] - if ok { - upgradeOK = upgradeMethods.Contains(methodName) - } - return upgradeOK -} - -func isMethodAllowedForMigrate(facadeName, methodName string) bool { - migrateOK := false - migrateMethods, ok := allowedMethodsForMigrate[facadeName] - if ok { - migrateOK = migrateMethods.Contains(methodName) - } - return migrateOK -} - -// These methods below are potentially called from a client with -// a different major version to the controller. -// As such we need to ensure we retain compatibility. - -// allowedDifferentClientMethods stores api calls we want to -// allow regardless of client or controller version. -var allowedDifferentClientMethods = map[string]set.Strings{ - "Client": set.NewStrings( - "FullStatus", - ), -} - -// allowedMethodsForUpgrade stores api calls -// that are not blocked when the connecting client has -// a major version greater than that of the controller. -var allowedMethodsForUpgrade = map[string]set.Strings{ - "Client": set.NewStrings( - "FindTools", - ), - "ModelUpgrader": set.NewStrings( - "UpgradeModel", - "AbortModelUpgrade", - ), - "ModelConfig": set.NewStrings( - "ModelGet", - ), - "Controller": set.NewStrings( - "ModelConfig", - "ControllerConfig", - "ControllerVersion", - "CloudSpec", - ), -} - -// allowedMethodsForMigrate stores api calls -// that are not blocked when the connecting client has -// a major version greater than that of the controller. -var allowedMethodsForMigrate = map[string]set.Strings{ - "UserManager": set.NewStrings( - "UserInfo", - ), - "ModelManager": set.NewStrings( - "ListModels", - "ModelInfo"), - "Controller": set.NewStrings( - "InitiateMigration", - "IdentityProviderURL", - ), -} diff --git a/apiserver/restrict_newer_client_test.go b/apiserver/restrict_newer_client_test.go deleted file mode 100644 index 4ecf3cfc136..00000000000 --- a/apiserver/restrict_newer_client_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2020 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package apiserver_test - -import ( - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - "github.com/juju/version/v2" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver" - "github.com/juju/juju/rpc/params" - "github.com/juju/juju/testing" - jujuversion "github.com/juju/juju/version" -) - -type restrictNewerClientSuite struct { - testing.BaseSuite - - callerVersion version.Number -} - -var _ = gc.Suite(&restrictNewerClientSuite{}) - -func (r *restrictNewerClientSuite) SetUpTest(c *gc.C) { - r.BaseSuite.SetUpTest(c) - // Patch to a big version so we avoid the whitelisted compatible - // versions by default. - r.PatchValue(&jujuversion.Current, version.MustParse("666.1.0")) - r.callerVersion = jujuversion.Current -} - -func (r *restrictNewerClientSuite) TestOldClientAllowedMethods(c *gc.C) { - r.callerVersion.Major = jujuversion.Current.Major - 1 - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - checkAllowed := func(facade, method string, version int) { - caller, err := root.FindMethod(facade, version, method) - c.Check(err, jc.ErrorIsNil) - c.Check(caller, gc.NotNil) - } - checkAllowed("Client", "FullStatus", 1) - checkAllowed("Pinger", "Ping", 1) - // Worker calls for migrations. - checkAllowed("MigrationTarget", "Prechecks", 1) - checkAllowed("UserManager", "UserInfo", 1) -} - -func (r *restrictNewerClientSuite) TestRecentNewerClientAllowedMethods(c *gc.C) { - r.assertNewerClientAllowedMethods(c, 0, true) - r.assertNewerClientAllowedMethods(c, 1, true) -} - -func (r *restrictNewerClientSuite) assertNewerClientAllowedMethods(c *gc.C, minor int, allowed bool) { - r.callerVersion.Major = jujuversion.Current.Major + 1 - r.callerVersion.Minor = minor - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - checkAllowed := func(facade, method string, version int) { - caller, err := root.FindMethod(facade, version, method) - if allowed { - c.Check(err, jc.ErrorIsNil) - c.Check(caller, gc.NotNil) - } else { - c.Check(err, gc.NotNil) - c.Check(caller, gc.IsNil) - } - } - checkAllowed("Client", "FullStatus", 1) - checkAllowed("Pinger", "Ping", 1) - // For migrations. - checkAllowed("MigrationTarget", "Prechecks", 1) - checkAllowed("UserManager", "UserInfo", 1) - // For upgrades. - checkAllowed("ModelUpgrader", "UpgradeModel", 1) -} - -func (r *restrictNewerClientSuite) TestOldClientUpgradeMethodDisallowed(c *gc.C) { - r.callerVersion.Major = jujuversion.Current.Major - 1 - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - caller, err := root.FindMethod("ModelUpgrader", 1, "UpgradeModel") - c.Assert(errors.HasType[*params.IncompatibleClientError](err), jc.IsTrue) - c.Assert(caller, gc.IsNil) -} - -func (r *restrictNewerClientSuite) TestReallyOldClientDisallowedMethod(c *gc.C) { - r.callerVersion.Major = jujuversion.Current.Major - 2 - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - caller, err := root.FindMethod("Client", 3, "FullStatus") - c.Assert(errors.HasType[*params.IncompatibleClientError](err), jc.IsTrue) - c.Assert(caller, gc.IsNil) -} - -func (r *restrictNewerClientSuite) TestReallyNewClientDisallowedMethod(c *gc.C) { - r.callerVersion.Major = jujuversion.Current.Major + 2 - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - caller, err := root.FindMethod("Client", 3, "FullStatus") - c.Assert(errors.HasType[*params.IncompatibleClientError](err), jc.IsTrue) - c.Assert(caller, gc.IsNil) -} - -func (r *restrictNewerClientSuite) TestAlwaysDisallowedMethod(c *gc.C) { - r.callerVersion.Major = jujuversion.Current.Major - 1 - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - caller, err := root.FindMethod("ModelConfig", 3, "ModelSet") - c.Assert(errors.HasType[*params.IncompatibleClientError](err), jc.IsTrue) - c.Assert(caller, gc.IsNil) -} - -func (r *restrictNewerClientSuite) TestWhitelistedClient(c *gc.C) { - r.assertWhitelistedClient(c, "2.9.35", false) - r.assertWhitelistedClient(c, "2.9.42", true) -} - -func (r *restrictNewerClientSuite) assertWhitelistedClient(c *gc.C, serverVers string, allowed bool) { - r.PatchValue(&jujuversion.Current, version.MustParse(serverVers)) - r.callerVersion = version.MustParse("3.0.0") - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(true, r.callerVersion) - caller, err := root.FindMethod("ModelConfig", 3, "ModelSet") - if allowed { - c.Check(err, jc.ErrorIsNil) - c.Check(caller, gc.NotNil) - } else { - c.Check(err, gc.NotNil) - c.Check(caller, gc.IsNil) - } -} - -func (r *restrictNewerClientSuite) TestAgentMethod(c *gc.C) { - r.PatchValue(&jujuversion.Current, version.MustParse("3.0.0")) - r.assertAgentMethod(c, "2.9.36", true) - r.assertAgentMethod(c, "2.9.31", false) -} - -func (r *restrictNewerClientSuite) assertAgentMethod(c *gc.C, agentVers string, allowed bool) { - r.callerVersion = version.MustParse(agentVers) - root := apiserver.TestingUpgradeOrMigrationOnlyRoot(false, r.callerVersion) - caller, err := root.FindMethod("Uniter", 15, "CurrentModel") - if allowed { - c.Check(err, jc.ErrorIsNil) - c.Check(caller, gc.NotNil) - } else { - c.Check(err, gc.NotNil) - c.Check(caller, gc.IsNil) - } -} diff --git a/apiserver/restrict_restore_test.go b/apiserver/restrict_restore_test.go deleted file mode 100644 index 9f8544acf03..00000000000 --- a/apiserver/restrict_restore_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package apiserver_test - -import ( - _ "github.com/juju/testing/checkers" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - "github.com/juju/juju/apiserver" - "github.com/juju/juju/testing" -) - -type restrictRestoreSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&restrictRestoreSuite{}) - -func (r *restrictRestoreSuite) TestAllowed(c *gc.C) { - root := apiserver.TestingAboutToRestoreRoot() - caller, err := root.FindMethod("Backups", 1, "Restore") - c.Assert(err, jc.ErrorIsNil) - c.Assert(caller, gc.NotNil) -} - -func (r *restrictRestoreSuite) TestNotAllowed(c *gc.C) { - root := apiserver.TestingAboutToRestoreRoot() - caller, err := root.FindMethod("Application", 1, "Deploy") - c.Assert(err, gc.ErrorMatches, "juju restore is in progress - functionality is limited to avoid data loss") - c.Assert(caller, gc.IsNil) -} diff --git a/apiserver/root.go b/apiserver/root.go index 8dec5df000d..d0656a51bb8 100644 --- a/apiserver/root.go +++ b/apiserver/root.go @@ -15,7 +15,6 @@ import ( "github.com/juju/errors" "github.com/juju/names/v4" "github.com/juju/rpcreflect" - "github.com/juju/version/v2" "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/facade" @@ -27,7 +26,6 @@ import ( "github.com/juju/juju/rpc" "github.com/juju/juju/rpc/params" "github.com/juju/juju/state" - jujuversion "github.com/juju/juju/version" ) var ( @@ -251,7 +249,6 @@ func restrictAPIRoot( apiRoot rpc.Root, model *state.Model, auth authResult, - clientVersion version.Number, ) (rpc.Root, error) { if !auth.controllerMachineLogin { // Controller agents are allowed to @@ -263,11 +260,6 @@ func restrictAPIRoot( return nil, errors.Trace(err) } apiRoot = restrictedRoot - // If the client version is different to the server version, - // add extra checks to ensure older incompatible clients cannot be used. - if clientVersion.Major != jujuversion.Current.Major { - apiRoot = restrictRoot(apiRoot, checkClientVersion(auth.userLogin, clientVersion)) - } } if auth.controllerOnlyLogin { apiRoot = restrictRoot(apiRoot, controllerFacadesOnly) diff --git a/rpc/params/apierror.go b/rpc/params/apierror.go index df77c3a8da5..6e1f85d93fb 100644 --- a/rpc/params/apierror.go +++ b/rpc/params/apierror.go @@ -12,7 +12,6 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/errors" "github.com/juju/loggo" - "github.com/juju/version/v2" "gopkg.in/macaroon.v2" ) @@ -25,24 +24,6 @@ var logger = loggo.GetLogger("juju.apiserver.params") // MigrationInProgressError signifies a migration is in progress. var MigrationInProgressError = errors.New(CodeMigrationInProgress) -// IncompatibleClientError signifies the connecting client is not -// compatible with the controller. -type IncompatibleClientError struct { - ServerVersion version.Number -} - -// Error implements error. -func (e *IncompatibleClientError) Error() string { - return fmt.Sprintf("client incompatible with server %v", e.ServerVersion) -} - -// AsMap returns the data for the RPC error Info field. -func (e *IncompatibleClientError) AsMap() map[string]interface{} { - return map[string]interface{}{ - "server-version": e.ServerVersion, - } -} - // Error is the type of error returned by any call to the state API. type Error struct { Message string `json:"message"` @@ -192,7 +173,6 @@ const ( CodeAlreadyExists = "already exists" CodeUpgradeInProgress = "upgrade in progress" CodeMigrationInProgress = "model migration in progress" - CodeIncompatibleClient = "incompatible client" CodeActionNotAvailable = "action no longer available" CodeOperationBlocked = "operation is blocked" CodeLeadershipClaimDenied = "leadership claim denied"