github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/application/updatebase.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application 5 6 import ( 7 "context" 8 "fmt" 9 10 "github.com/juju/errors" 11 "github.com/juju/names/v5" 12 13 apiservererrors "github.com/juju/juju/apiserver/errors" 14 "github.com/juju/juju/charmhub" 15 "github.com/juju/juju/charmhub/transport" 16 corebase "github.com/juju/juju/core/base" 17 corecharm "github.com/juju/juju/core/charm" 18 "github.com/juju/juju/rpc/params" 19 "github.com/juju/juju/state" 20 ) 21 22 // CharmhubClient represents a way for querying the charmhub api for information 23 // about the application charm. 24 type CharmhubClient interface { 25 Refresh(ctx context.Context, config charmhub.RefreshConfig) ([]transport.RefreshResponse, error) 26 } 27 28 // UpdateBase defines an interface for interacting with updating a base. 29 type UpdateBase interface { 30 31 // UpdateBase attempts to update an application base for deploying new 32 // units. 33 UpdateBase(string, corebase.Base, bool) error 34 } 35 36 // UpdateBaseState defines a common set of functions for retrieving state 37 // objects. 38 type UpdateBaseState interface { 39 // Application returns a list of all the applications for a 40 // given machine. This includes all the subordinates. 41 Application(string) (Application, error) 42 } 43 44 // UpdateBaseValidator defines an application validator. 45 type UpdateBaseValidator interface { 46 // ValidateApplication attempts to validate an application for 47 // a given base. Using force to allow the overriding of the error to 48 // ensure all application validate. 49 // 50 // I do question if you actually need to validate anything if force is 51 // employed here? 52 ValidateApplication(application Application, base corebase.Base, force bool) error 53 } 54 55 // UpdateBaseAPI provides the update series API facade for any given version. 56 // It is expected that any API parameter changes should be performed before 57 // entering the API. 58 type UpdateBaseAPI struct { 59 state UpdateBaseState 60 validator UpdateBaseValidator 61 } 62 63 // NewUpdateBaseAPI creates a new UpdateBaseAPI 64 func NewUpdateBaseAPI( 65 state UpdateBaseState, 66 validator UpdateBaseValidator, 67 ) *UpdateBaseAPI { 68 return &UpdateBaseAPI{ 69 state: state, 70 validator: validator, 71 } 72 } 73 74 func (a *UpdateBaseAPI) UpdateBase(tag string, base corebase.Base, force bool) error { 75 if base.String() == "" { 76 return errors.BadRequestf("base missing from args") 77 } 78 applicationTag, err := names.ParseApplicationTag(tag) 79 if err != nil { 80 return errors.Trace(err) 81 } 82 app, err := a.state.Application(applicationTag.Id()) 83 if err != nil { 84 return errors.Trace(err) 85 } 86 if !app.IsPrincipal() { 87 return ¶ms.Error{ 88 Message: fmt.Sprintf("%q is a subordinate application, update-series not supported", applicationTag.Id()), 89 Code: params.CodeNotSupported, 90 } 91 } 92 93 if err := a.validator.ValidateApplication(app, base, force); err != nil { 94 return errors.Trace(err) 95 } 96 97 return app.UpdateApplicationBase(state.Base{ 98 OS: base.OS, Channel: base.Channel.String(), 99 }, force) 100 } 101 102 type updateSeriesValidator struct { 103 localValidator UpdateBaseValidator 104 remoteValidator UpdateBaseValidator 105 } 106 107 func makeUpdateSeriesValidator(client CharmhubClient) updateSeriesValidator { 108 return updateSeriesValidator{ 109 localValidator: stateSeriesValidator{}, 110 remoteValidator: charmhubSeriesValidator{ 111 client: client, 112 }, 113 } 114 } 115 116 func (s updateSeriesValidator) ValidateApplication(app Application, base corebase.Base, force bool) error { 117 // This is not a charmhub charm, so we can fallback to querying state 118 // for the supported series. 119 if origin := app.CharmOrigin(); origin == nil || !corecharm.CharmHub.Matches(origin.Source) { 120 return s.localValidator.ValidateApplication(app, base, force) 121 } 122 123 return s.remoteValidator.ValidateApplication(app, base, force) 124 } 125 126 // stateSeriesValidator validates an application using the state (database) 127 // version of the charm. 128 // NOTE: stateSeriesValidator also exists in apiserver/facades/client/machinemanager/upgrade_series.go, 129 // When making changes here, review the copy for required changes as well. 130 type stateSeriesValidator struct{} 131 132 // ValidateApplication attempts to validate an applications for 133 // a given base. 134 func (s stateSeriesValidator) ValidateApplication(application Application, base corebase.Base, force bool) error { 135 ch, _, err := application.Charm() 136 if err != nil { 137 return errors.Trace(err) 138 } 139 supportedBases, err := corecharm.ComputedBases(ch) 140 if err != nil { 141 return errors.Trace(err) 142 } 143 if len(supportedBases) == 0 { 144 err := errors.NewNotSupported(nil, fmt.Sprintf("charm %q does not support any bases. Not valid", ch.Meta().Name)) 145 return apiservererrors.ServerError(err) 146 } 147 _, baseSupportedErr := corecharm.BaseForCharm(base, supportedBases) 148 if baseSupportedErr != nil && !force { 149 return apiservererrors.NewErrIncompatibleBase(supportedBases, base, ch.Meta().Name) 150 } 151 return nil 152 } 153 154 // NOTE: charmhubSeriesValidator also exists in apiserver/facades/client/machinemanager/upgrade_series.go, 155 // When making changes here, review the copy for required changes as well. 156 type charmhubSeriesValidator struct { 157 client CharmhubClient 158 } 159 160 // ValidateApplication attempts to validate an application for 161 // a given base. 162 func (s charmhubSeriesValidator) ValidateApplication(application Application, base corebase.Base, force bool) error { 163 // We can be assured that the charm origin is not nil, because we 164 // guarded against it before. 165 origin := application.CharmOrigin() 166 rev := origin.Revision 167 if rev == nil { 168 return errors.Errorf("no revision found for application %q", application.Name()) 169 } 170 171 cfg, err := charmhub.DownloadOneFromRevision(origin.ID, *rev) 172 if err != nil { 173 return errors.Trace(err) 174 } 175 176 refreshResp, err := s.client.Refresh(context.TODO(), cfg) 177 if err != nil { 178 return errors.Trace(err) 179 } 180 if len(refreshResp) != 1 { 181 return errors.Errorf("unexpected number of responses %d for applications 1", len(refreshResp)) 182 } 183 for _, resp := range refreshResp { 184 if err := resp.Error; err != nil && !force { 185 return errors.Annotatef(err, "unable to locate application with base %s", base.DisplayString()) 186 } 187 } 188 // DownloadOneFromRevision does not take a base, however the response contains the bases 189 // supported by the given revision. Validate against provided series. 190 channelToValidate := base.Channel.Track 191 for _, resp := range refreshResp { 192 var found bool 193 for _, base := range resp.Entity.Bases { 194 if channelToValidate == base.Channel || force { 195 found = true 196 break 197 } 198 } 199 if !found { 200 return errors.Errorf("charm %q does not support %s, force not used", resp.Name, base.DisplayString()) 201 } 202 } 203 return nil 204 }