github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/jujuclient/models.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package jujuclient
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/featureflag"
    13  	"github.com/juju/names/v5"
    14  	"github.com/juju/utils/v3"
    15  	"gopkg.in/yaml.v2"
    16  
    17  	"github.com/juju/juju/core/model"
    18  	"github.com/juju/juju/feature"
    19  	"github.com/juju/juju/juju/osenv"
    20  )
    21  
    22  // JujuModelsPath is the location where models information is
    23  // expected to be found.
    24  func JujuModelsPath() string {
    25  	// TODO(axw) models.yaml should go into XDG_CACHE_HOME.
    26  	return osenv.JujuXDGDataHomePath("models.yaml")
    27  }
    28  
    29  // ReadModelsFile loads all models defined in a given file.
    30  // If the file is not found, it is not an error.
    31  func ReadModelsFile(file string) (map[string]*ControllerModels, error) {
    32  	data, err := os.ReadFile(file)
    33  	if err != nil {
    34  		if os.IsNotExist(err) {
    35  			return nil, nil
    36  		}
    37  		return nil, err
    38  	}
    39  	models, err := ParseModels(data)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	if err := migrateLocalModelUsers(models); err != nil {
    44  		return nil, err
    45  	}
    46  	if err := addModelType(models); err != nil {
    47  		return nil, err
    48  	}
    49  	if featureflag.Enabled(feature.Branches) || featureflag.Enabled(feature.Generations) {
    50  		if err := addGeneration(models); err != nil {
    51  			return nil, err
    52  		}
    53  	}
    54  	return models, nil
    55  }
    56  
    57  // addGeneration add missing generation version data if necessary.
    58  // Default to 'current'.
    59  func addGeneration(models map[string]*ControllerModels) error {
    60  	changes := false
    61  	for _, cm := range models {
    62  		for name, m := range cm.Models {
    63  			if m.ActiveBranch == "" {
    64  				changes = true
    65  				m.ActiveBranch = model.GenerationMaster
    66  				cm.Models[name] = m
    67  			}
    68  		}
    69  	}
    70  	if changes {
    71  		return WriteModelsFile(models)
    72  	}
    73  	return nil
    74  }
    75  
    76  // addModelType adds missing model type data if necessary.
    77  func addModelType(models map[string]*ControllerModels) error {
    78  	changes := false
    79  	for _, cm := range models {
    80  		for name, m := range cm.Models {
    81  			if m.ModelType == "" {
    82  				changes = true
    83  				m.ModelType = model.IAAS
    84  				cm.Models[name] = m
    85  			}
    86  		}
    87  	}
    88  	if changes {
    89  		return WriteModelsFile(models)
    90  	}
    91  	return nil
    92  }
    93  
    94  // migrateLocalModelUsers strips any @local domains from any qualified model names.
    95  func migrateLocalModelUsers(usermodels map[string]*ControllerModels) error {
    96  	changes := false
    97  	for _, modelDetails := range usermodels {
    98  		for name, model := range modelDetails.Models {
    99  			migratedName, changed, err := migrateModelName(name)
   100  			if err != nil {
   101  				return errors.Trace(err)
   102  			}
   103  			if !changed {
   104  				continue
   105  			}
   106  			delete(modelDetails.Models, name)
   107  			modelDetails.Models[migratedName] = model
   108  			changes = true
   109  		}
   110  		migratedName, changed, err := migrateModelName(modelDetails.CurrentModel)
   111  		if err != nil {
   112  			return errors.Trace(err)
   113  		}
   114  		if !changed {
   115  			continue
   116  		}
   117  		modelDetails.CurrentModel = migratedName
   118  	}
   119  	if changes {
   120  		return WriteModelsFile(usermodels)
   121  	}
   122  	return nil
   123  }
   124  
   125  func migrateModelName(legacyName string) (string, bool, error) {
   126  	i := strings.IndexRune(legacyName, '/')
   127  	if i < 0 {
   128  		return legacyName, false, nil
   129  	}
   130  	owner := legacyName[:i]
   131  	if !names.IsValidUser(owner) {
   132  		return "", false, errors.NotValidf("user name %q", owner)
   133  	}
   134  	if !strings.HasSuffix(owner, "@local") {
   135  		return legacyName, false, nil
   136  	}
   137  	rawModelName := legacyName[i+1:]
   138  	return JoinOwnerModelName(names.NewUserTag(owner), rawModelName), true, nil
   139  }
   140  
   141  // WriteModelsFile marshals to YAML details of the given models
   142  // and writes it to the models file.
   143  func WriteModelsFile(models map[string]*ControllerModels) error {
   144  	data, err := yaml.Marshal(modelsCollection{models})
   145  	if err != nil {
   146  		return errors.Annotate(err, "cannot marshal models")
   147  	}
   148  	return utils.AtomicWriteFile(JujuModelsPath(), data, os.FileMode(0600))
   149  }
   150  
   151  // ParseModels parses the given YAML bytes into models metadata.
   152  func ParseModels(data []byte) (map[string]*ControllerModels, error) {
   153  	var result modelsCollection
   154  	err := yaml.Unmarshal(data, &result)
   155  	if err != nil {
   156  		return nil, errors.Annotate(err, "cannot unmarshal models")
   157  	}
   158  	return result.ControllerModels, nil
   159  }
   160  
   161  type modelsCollection struct {
   162  	ControllerModels map[string]*ControllerModels `yaml:"controllers"`
   163  }
   164  
   165  // ControllerModels stores per-controller account-model information.
   166  type ControllerModels struct {
   167  	// Models is the collection of models for the account, indexed
   168  	// by model name. This should be treated as a cache only, and
   169  	// not the complete set of models for the account.
   170  	Models map[string]ModelDetails `yaml:"models,omitempty"`
   171  
   172  	// CurrentModel is the name of the active model for the account.
   173  	CurrentModel string `yaml:"current-model,omitempty"`
   174  }
   175  
   176  // JoinOwnerModelName returns a model name qualified with the model owner.
   177  func JoinOwnerModelName(owner names.UserTag, modelName string) string {
   178  	return fmt.Sprintf("%s/%s", owner.Id(), modelName)
   179  }
   180  
   181  // IsQualifiedModelName returns true if the provided model name is qualified
   182  // with an owner. The name is assumed to be either a valid qualified model
   183  // name, or a valid unqualified model name.
   184  func IsQualifiedModelName(name string) bool {
   185  	return strings.ContainsRune(name, '/')
   186  }
   187  
   188  // SplitModelName splits a qualified model name into the model and owner
   189  // name components.
   190  func SplitModelName(name string) (string, names.UserTag, error) {
   191  	i := strings.IndexRune(name, '/')
   192  	if i < 0 {
   193  		return "", names.UserTag{}, errors.NotValidf("unqualified model name %q", name)
   194  	}
   195  	owner := name[:i]
   196  	if !names.IsValidUser(owner) {
   197  		return "", names.UserTag{}, errors.NotValidf("user name %q", owner)
   198  	}
   199  	name = name[i+1:]
   200  	return name, names.NewUserTag(owner), nil
   201  }