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  }