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 }