github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/common/credentialcommon/modelcredential.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package credentialcommon 5 6 import ( 7 stdcontext "context" 8 9 "github.com/juju/collections/set" 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/caas" 15 "github.com/juju/juju/cloud" 16 "github.com/juju/juju/environs" 17 environscloudspec "github.com/juju/juju/environs/cloudspec" 18 "github.com/juju/juju/environs/context" 19 "github.com/juju/juju/environs/instances" 20 "github.com/juju/juju/rpc/params" 21 "github.com/juju/juju/state" 22 ) 23 24 // ValidateExistingModelCredential checks if the cloud credential that a given 25 // model uses is valid for it. For IAAS models, if the modelMigrationCheck is 26 // disabled, then it will not perform the mapping of the instances on the clouud 27 // to the machines on the model, and deem the credential valid if it can be used 28 // to just access the instances on the cloud. Otherwise the instances will be 29 // mapped against the machines on the model. Furthermore, normally it's valid to 30 // have more instances than machines, but if the checkCloudInstances is enabled, 31 // then a 1:1 mapping is expected to deem the credential valid. 32 func ValidateExistingModelCredential( 33 backend PersistentBackend, 34 callCtx context.ProviderCallContext, 35 checkCloudInstances bool, 36 modelMigrationCheck bool) (params.ErrorResults, error) { 37 model, err := backend.Model() 38 if err != nil { 39 return params.ErrorResults{}, errors.Trace(err) 40 } 41 42 credentialTag, isSet := model.CloudCredentialTag() 43 if !isSet { 44 return params.ErrorResults{}, nil 45 } 46 47 storedCredential, err := backend.CloudCredential(credentialTag) 48 if err != nil { 49 return params.ErrorResults{}, errors.Trace(err) 50 } 51 52 if !storedCredential.IsValid() { 53 return params.ErrorResults{}, errors.NotValidf("credential %q", storedCredential.Name) 54 } 55 credential := cloud.NewCredential(cloud.AuthType(storedCredential.AuthType), storedCredential.Attributes) 56 return ValidateNewModelCredential(backend, callCtx, credentialTag, 57 &credential, checkCloudInstances, modelMigrationCheck) 58 } 59 60 // ValidateNewModelCredential checks if a new cloud credential could be valid 61 // for a given model. For IAAS models, if the modelMigrationCheck is disabled, 62 // then it will not perform the mapping of the instances on the clouud to the 63 // machines on the model, and deem the credential valid if it can be used to 64 // just access the instances on the cloud. Otherwise the instances will be 65 // mapped against the machines on the model. Furthermore, normally it's valid to 66 // have more instances than machines, but if the checkCloudInstances is enabled, 67 // then a 1:1 mapping is expected to deem the credential valid. 68 func ValidateNewModelCredential( 69 backend PersistentBackend, 70 callCtx context.ProviderCallContext, 71 credentialTag names.CloudCredentialTag, 72 credential *cloud.Credential, 73 checkCloudInstances bool, 74 modelMigrationCheck bool) (params.ErrorResults, error) { 75 openParams, err := buildOpenParams(backend, credentialTag, credential) 76 if err != nil { 77 return params.ErrorResults{}, errors.Trace(err) 78 } 79 model, err := backend.Model() 80 if err != nil { 81 return params.ErrorResults{}, errors.Trace(err) 82 } 83 switch model.Type() { 84 case state.ModelTypeCAAS: 85 return checkCAASModelCredential(openParams) 86 case state.ModelTypeIAAS: 87 return checkIAASModelCredential(openParams, backend, callCtx, 88 checkCloudInstances, modelMigrationCheck) 89 default: 90 return params.ErrorResults{}, errors.NotSupportedf("model type %q", model.Type()) 91 } 92 } 93 94 func checkCAASModelCredential(brokerParams environs.OpenParams) (params.ErrorResults, error) { 95 broker, err := newCAASBroker(stdcontext.TODO(), brokerParams) 96 if err != nil { 97 return params.ErrorResults{}, errors.Trace(err) 98 } 99 100 if err = broker.CheckCloudCredentials(); err != nil { 101 return params.ErrorResults{}, errors.Trace(err) 102 } 103 return params.ErrorResults{}, nil 104 } 105 106 // checkIAASModelCredential checks if the cloud credential that a given model 107 // uses is valid for it. if the modelMigrationCheck is disabled, then it will 108 // not perform the mapping of the instances on the clouud to the machines on the 109 // model, and deem the credential valid if it can be used to just access the 110 // instances on the cloud. Otherwise the instances will be mapped against the 111 // machines on the model. Furthermore, normally it's valid to have more 112 // instances than machines, but if the checkCloudInstances is enabled, then a 113 // 1:1 mapping is expected to deem the credential valid. 114 func checkIAASModelCredential( 115 openParams environs.OpenParams, 116 backend PersistentBackend, 117 callCtx context.ProviderCallContext, 118 checkCloudInstances bool, 119 modelMigrationCheck bool) (params.ErrorResults, error) { 120 env, err := newEnv(callCtx, openParams) 121 if err != nil { 122 return params.ErrorResults{}, errors.Trace(err) 123 } 124 125 // Check that we can see all machines' instances regardless of their state 126 // as perceived by the cloud, i.e. this call will return all non-terminated 127 // instances. 128 instances, err := env.AllInstances(callCtx) 129 // If we're not performing this check for model migrations; then being able 130 // to get the instances is proof enough that the credential is valid 131 // (authenticated, authorization is a different concern), no need to check 132 // the mapping between instances and machines. 133 if err != nil { 134 return params.ErrorResults{Results: []params.ErrorResult{{ 135 Error: apiservererrors.ServerError(errors.Annotate(err, "receiving instances from provider"))}}, 136 }, errors.Trace(err) 137 } 138 139 if !modelMigrationCheck { 140 return params.ErrorResults{}, nil 141 } 142 143 // We only check persisted machines vs known cloud instances. In the future, 144 // this check may be extended to other cloud resources, entities and 145 // operation-level authorisations such as interfaces, ability to CRUD 146 // storage, etc. 147 return checkMachineInstances(backend, checkCloudInstances, instances) 148 } 149 150 // checkMachineInstances compares model machines from state with the ones 151 // reported by the provider using supplied credential. This only makes sense for 152 // non-k8s providers. 153 func checkMachineInstances( 154 backend PersistentBackend, 155 checkCloudInstances bool, 156 instances []instances.Instance) (params.ErrorResults, error) { 157 fail := func(original error) (params.ErrorResults, error) { 158 return params.ErrorResults{}, original 159 } 160 161 // Get machines from state 162 machines, err := backend.AllMachines() 163 if err != nil { 164 return fail(errors.Trace(err)) 165 } 166 167 var results []params.ErrorResult 168 169 serverError := func(received error) params.ErrorResult { 170 return params.ErrorResult{Error: apiservererrors.ServerError(received)} 171 } 172 173 machinesByInstance := make(map[string]string) 174 for _, machine := range machines { 175 if machine.IsContainer() { 176 // Containers don't correspond to instances at the 177 // provider level. 178 continue 179 } 180 if manual, err := machine.IsManual(); err != nil { 181 return fail(errors.Trace(err)) 182 } else if manual { 183 continue 184 } 185 instanceId, err := machine.InstanceId() 186 if errors.IsNotProvisioned(err) { 187 // Skip over this machine; we wouldn't expect the cloud 188 // to know about it. 189 continue 190 } else if err != nil { 191 results = append(results, serverError(errors.Annotatef(err, 192 "getting instance id for machine %s", machine.Id()))) 193 continue 194 } 195 machinesByInstance[string(instanceId)] = machine.Id() 196 } 197 198 // From here, we cross examine all machines we know about with all the 199 // instances we can reach and ensure that they correspond 1:1. This is 200 // useful for model migration, for example, since we want to know if we have 201 // moved the known universe correctly. 202 instanceIds := set.NewStrings() 203 for _, instance := range instances { 204 id := string(instance.Id()) 205 instanceIds.Add(id) 206 if checkCloudInstances { 207 if _, found := machinesByInstance[id]; !found { 208 results = append(results, serverError(errors.Errorf("no machine with instance %q", id))) 209 } 210 } 211 } 212 213 for instanceId, name := range machinesByInstance { 214 if !instanceIds.Contains(instanceId) { 215 results = append(results, serverError(errors.Errorf("couldn't find instance %q for machine %s", instanceId, name))) 216 } 217 } 218 219 return params.ErrorResults{Results: results}, nil 220 } 221 222 var ( 223 newEnv = environs.New 224 newCAASBroker = caas.New 225 ) 226 227 func buildOpenParams( 228 backend PersistentBackend, 229 credentialTag names.CloudCredentialTag, 230 credential *cloud.Credential) (environs.OpenParams, error) { 231 fail := func(original error) (environs.OpenParams, error) { 232 return environs.OpenParams{}, original 233 } 234 235 model, err := backend.Model() 236 if err != nil { 237 return fail(errors.Trace(err)) 238 } 239 240 modelCloud, err := backend.Cloud(model.CloudName()) 241 if err != nil { 242 return fail(errors.Trace(err)) 243 } 244 245 err = model.ValidateCloudCredential(credentialTag, *credential) 246 if err != nil { 247 return fail(errors.Trace(err)) 248 } 249 250 tempCloudSpec, err := environscloudspec.MakeCloudSpec(modelCloud, 251 model.CloudRegion(), credential) 252 if err != nil { 253 return fail(errors.Trace(err)) 254 } 255 256 cfg, err := model.Config() 257 if err != nil { 258 return fail(errors.Trace(err)) 259 } 260 261 controllerConfig, err := backend.ControllerConfig() 262 if err != nil { 263 return fail(errors.Trace(err)) 264 } 265 return environs.OpenParams{ 266 ControllerUUID: controllerConfig.ControllerUUID(), 267 Cloud: tempCloudSpec, 268 Config: cfg, 269 }, nil 270 }