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  }