github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/instance/lifecycle/patch.go (about) 1 package lifecycle 2 3 import ( 4 "io" 5 "net/http" 6 "net/url" 7 "reflect" 8 "strings" 9 "time" 10 11 "github.com/cozy/cozy-stack/model/cloudery" 12 "github.com/cozy/cozy-stack/model/instance" 13 "github.com/cozy/cozy-stack/pkg/couchdb" 14 "github.com/cozy/cozy-stack/pkg/logger" 15 "github.com/labstack/echo/v4" 16 ) 17 18 var managerHTTPClient = &http.Client{Timeout: 30 * time.Second} 19 20 // AskReupload is the function that will be called when the disk quota is 21 // increased to ask reuploading files from the sharings. A package variable is 22 // used to avoid a dependency on the model/sharing package (which would lead to 23 // circular import issue). 24 var AskReupload func(*instance.Instance) error 25 26 // Patch updates the given instance with the specified options if necessary. It 27 // can also update the settings document if provided in the options. 28 func Patch(i *instance.Instance, opts *Options) error { 29 opts.Domain = i.Domain 30 settings, err := buildSettings(i, opts) 31 if err != nil { 32 return err 33 } 34 35 for { 36 var err error 37 if i == nil { 38 i, err = GetInstance(opts.Domain) 39 if err != nil { 40 return err 41 } 42 } 43 44 needUpdate := false 45 needSharingReupload := false 46 47 if opts.Locale != "" && opts.Locale != i.Locale { 48 i.Locale = opts.Locale 49 needUpdate = true 50 } 51 52 if opts.Blocked != nil && *opts.Blocked != i.Blocked { 53 i.Blocked = *opts.Blocked 54 needUpdate = true 55 } 56 57 if opts.BlockingReason != "" && opts.BlockingReason != i.BlockingReason { 58 i.BlockingReason = opts.BlockingReason 59 needUpdate = true 60 } 61 62 if aliases := opts.DomainAliases; aliases != nil { 63 i.DomainAliases, err = checkAliases(i, aliases) 64 if err != nil { 65 return err 66 } 67 needUpdate = true 68 } 69 70 if opts.UUID != "" && opts.UUID != i.UUID { 71 i.UUID = opts.UUID 72 needUpdate = true 73 } 74 75 if opts.OIDCID != "" && opts.OIDCID != i.OIDCID { 76 i.OIDCID = opts.OIDCID 77 needUpdate = true 78 } 79 80 if opts.FranceConnectID != "" && opts.FranceConnectID != i.FranceConnectID { 81 i.FranceConnectID = opts.FranceConnectID 82 needUpdate = true 83 } 84 85 if opts.MagicLink != nil && *opts.MagicLink != i.MagicLink { 86 i.MagicLink = *opts.MagicLink 87 needUpdate = true 88 } 89 90 if opts.ContextName != "" && opts.ContextName != i.ContextName { 91 i.ContextName = opts.ContextName 92 needUpdate = true 93 } 94 95 if len(opts.Sponsorships) != 0 { 96 i.Sponsorships = opts.Sponsorships 97 needUpdate = true 98 } 99 100 if opts.AuthMode != "" { 101 var authMode instance.AuthMode 102 authMode, err = instance.StringToAuthMode(opts.AuthMode) 103 if err != nil { 104 return err 105 } 106 if i.AuthMode != authMode { 107 i.AuthMode = authMode 108 needUpdate = true 109 } 110 } 111 112 if opts.DiskQuota > 0 && opts.DiskQuota != i.BytesDiskQuota { 113 needUpdate = true 114 needSharingReupload = opts.DiskQuota > i.BytesDiskQuota 115 i.BytesDiskQuota = opts.DiskQuota 116 } else if opts.DiskQuota == -1 { 117 i.BytesDiskQuota = 0 118 needUpdate = true 119 } 120 121 if opts.AutoUpdate != nil && !(*opts.AutoUpdate) != i.NoAutoUpdate { 122 i.NoAutoUpdate = !(*opts.AutoUpdate) 123 needUpdate = true 124 } 125 126 if opts.OnboardingFinished != nil && *opts.OnboardingFinished != i.OnboardingFinished { 127 i.OnboardingFinished = *opts.OnboardingFinished 128 needUpdate = true 129 } 130 131 if opts.TOSLatest != "" { 132 if _, date, ok := instance.ParseTOSVersion(opts.TOSLatest); !ok || date.IsZero() { 133 return instance.ErrBadTOSVersion 134 } 135 if i.TOSLatest != opts.TOSLatest { 136 if i.CheckTOSNotSigned(opts.TOSLatest) { 137 i.TOSLatest = opts.TOSLatest 138 needUpdate = true 139 } 140 } 141 } 142 143 if opts.TOSSigned != "" { 144 if _, _, ok := instance.ParseTOSVersion(opts.TOSSigned); !ok { 145 return instance.ErrBadTOSVersion 146 } 147 if i.TOSSigned != opts.TOSSigned { 148 i.TOSSigned = opts.TOSSigned 149 if !i.CheckTOSNotSigned() { 150 i.TOSLatest = "" 151 } 152 needUpdate = true 153 } 154 } 155 156 if !needUpdate { 157 break 158 } 159 160 err = update(i) 161 if couchdb.IsConflictError(err) { 162 i = nil 163 continue 164 } 165 if err != nil { 166 return err 167 } 168 if needSharingReupload && AskReupload != nil { 169 go func() { 170 inst := i.Clone().(*instance.Instance) 171 if err := AskReupload(inst); err != nil { 172 inst.Logger().WithNamespace("lifecycle"). 173 Warnf("sharing.AskReupload failed with %s", err) 174 } 175 }() 176 } 177 break 178 } 179 180 // Update the settings doc 181 if ok := needsSettingsUpdate(i, settings.M); ok { 182 if err := couchdb.UpdateDoc(i, settings); err != nil { 183 return err 184 } 185 186 if !opts.FromCloudery { 187 email, _ := settings.M["email"].(string) 188 publicName, _ := settings.M["public_name"].(string) 189 190 err = cloudery.SaveInstance(i, &cloudery.SaveCmd{ 191 Locale: i.Locale, 192 Email: email, 193 PublicName: publicName, 194 }) 195 if err != nil { 196 i.Logger().Errorf("Error during cloudery settings update %s", err) 197 } 198 } 199 } 200 201 if debug := opts.Debug; debug != nil { 202 var err error 203 if *debug { 204 err = logger.AddDebugDomain(i.Domain, 24*time.Hour) 205 } else { 206 err = logger.RemoveDebugDomain(i.Domain) 207 } 208 if err != nil { 209 return err 210 } 211 } 212 213 return nil 214 } 215 216 // needsSettingsUpdate compares the old instance io.cozy.settings with the new 217 // bunch of settings and tells if it needs an update 218 func needsSettingsUpdate(inst *instance.Instance, newSettings map[string]interface{}) bool { 219 oldSettings, err := inst.SettingsDocument() 220 if err != nil { 221 return false 222 } 223 224 if oldSettings.M == nil { 225 return true 226 } 227 228 for k, newValue := range newSettings { 229 if k == "_id" || k == "_rev" { 230 continue 231 } 232 // Check if we have the key in old settings and the value is different, 233 // or if we don't have the key at all 234 if oldValue, ok := oldSettings.M[k]; !ok || !reflect.DeepEqual(oldValue, newValue) { 235 return true 236 } 237 } 238 239 // Handles if a key was removed in the new settings but exists in the old 240 // settings, and therefore needs an update 241 for oldKey := range oldSettings.M { 242 if _, ok := newSettings[oldKey]; !ok { 243 return true 244 } 245 } 246 247 return false 248 } 249 250 // Block function blocks an instance with an optional reason parameter 251 func Block(inst *instance.Instance, reason ...string) error { 252 var r string 253 if len(reason) == 1 { 254 r = reason[0] 255 } else { 256 r = instance.BlockedUnknown.Code 257 } 258 inst.Blocked = true 259 inst.BlockingReason = r 260 return update(inst) 261 } 262 263 // Unblock reverts the blocking of an instance 264 func Unblock(inst *instance.Instance) error { 265 inst.Blocked = false 266 inst.BlockingReason = "" 267 return update(inst) 268 } 269 270 // ManagerSignTOS make a request to the manager in order to finalize the TOS 271 // signing flow. 272 func ManagerSignTOS(inst *instance.Instance, originalReq *http.Request) error { 273 if inst.TOSLatest == "" { 274 return nil 275 } 276 split := strings.SplitN(inst.TOSLatest, "-", 2) 277 if len(split) != 2 { 278 return nil 279 } 280 u, err := inst.ManagerURL(instance.ManagerTOSURL) 281 if err != nil { 282 return Patch(inst, &Options{TOSSigned: inst.TOSLatest}) 283 } 284 form := url.Values{"version": {split[0]}} 285 res, err := doManagerRequest(http.MethodPut, u, form, originalReq) 286 if err != nil { 287 return err 288 } 289 return res.Body.Close() 290 } 291 292 func doManagerRequest(method string, url string, form url.Values, originalReq *http.Request) (*http.Response, error) { 293 var body io.Reader 294 if form != nil { 295 body = strings.NewReader(form.Encode()) 296 } 297 req, err := http.NewRequest(method, url, body) 298 if err != nil { 299 return nil, err 300 } 301 if form != nil { 302 req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm) 303 } 304 if originalReq != nil { 305 var ip string 306 if forwardedFor := req.Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" { 307 ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0]) 308 } 309 if ip == "" { 310 ip = req.RemoteAddr 311 } 312 req.Header.Set(echo.HeaderXForwardedFor, ip) 313 } 314 return managerHTTPClient.Do(req) 315 }