github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/exec/service.go (about) 1 package exec 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 11 "github.com/cozy/cozy-stack/model/app" 12 "github.com/cozy/cozy-stack/model/instance" 13 "github.com/cozy/cozy-stack/model/job" 14 "github.com/cozy/cozy-stack/pkg/appfs" 15 "github.com/cozy/cozy-stack/pkg/config/config" 16 "github.com/cozy/cozy-stack/pkg/consts" 17 "github.com/cozy/cozy-stack/pkg/couchdb" 18 "github.com/cozy/cozy-stack/pkg/logger" 19 "github.com/spf13/afero" 20 ) 21 22 // ServiceOptions contains the options to execute a service. 23 type ServiceOptions struct { 24 Slug string `json:"slug"` // The application slug 25 Name string `json:"name"` // The service name 26 Fields json.RawMessage `json:"fields"` // Custom fields 27 File string `json:"service_file"` 28 29 Message *ServiceOptions `json:"message"` 30 } 31 32 type serviceWorker struct { 33 man *app.WebappManifest 34 slug string 35 name string 36 fields json.RawMessage 37 workDir string 38 } 39 40 func (w *serviceWorker) PrepareWorkDir(ctx *job.TaskContext, i *instance.Instance) (workDir string, cleanDir func(), err error) { 41 cleanDir = func() {} 42 opts := &ServiceOptions{} 43 if err = ctx.UnmarshalMessage(&opts); err != nil { 44 return 45 } 46 if opts.Message != nil { 47 opts = opts.Message 48 } 49 50 slug := opts.Slug 51 name := opts.Name 52 fields := opts.Fields 53 54 man, err := app.GetWebappBySlugAndUpdate(i, slug, 55 app.Copier(consts.WebappType, i), i.Registries()) 56 if err != nil { 57 if errors.Is(err, app.ErrNotFound) { 58 err = job.BadTriggerError{Err: err} 59 } 60 return 61 } 62 63 w.slug = slug 64 w.name = name 65 w.fields = fields 66 67 // Upgrade "installed" to "ready" 68 if err = app.UpgradeInstalledState(i, man); err != nil { 69 return 70 } 71 72 if man.State() != app.Ready { 73 err = errors.New("Application is not ready") 74 return 75 } 76 77 var service *app.Service 78 var ok bool 79 services := man.Services() 80 if name != "" { 81 service, ok = services[name] 82 } else { 83 for _, s := range services { 84 if s.File == opts.File { 85 service, ok = s, true 86 break 87 } 88 } 89 } 90 if !ok { 91 err = job.BadTriggerError{Err: fmt.Errorf("Service %q was not found", name)} 92 return 93 } 94 // Check if the trigger is orphan 95 if triggerID, ok := ctx.TriggerID(); ok && service.TriggerID != "" { 96 if triggerID != service.TriggerID { 97 // Check if this is another trigger for the same declared service. 98 // Note that the trigger may be not found if it was an @at. 99 var tInfos job.TriggerInfos 100 if err = couchdb.GetDoc(i, consts.Triggers, triggerID, &tInfos); err == nil { 101 var msg ServiceOptions 102 err = json.Unmarshal(tInfos.Message, &msg) 103 if err != nil { 104 err = job.BadTriggerError{Err: fmt.Errorf("Trigger %q has bad message structure", triggerID)} 105 return 106 } 107 if msg.Name != name { 108 err = job.BadTriggerError{Err: fmt.Errorf("Trigger %q is orphan", triggerID)} 109 return 110 } 111 } 112 } 113 } 114 115 w.man = man 116 117 osFS := afero.NewOsFs() 118 workDir, err = afero.TempDir(osFS, "", "service-"+slug) 119 if err != nil { 120 return 121 } 122 cleanDir = func() { 123 _ = os.RemoveAll(workDir) 124 } 125 w.workDir = workDir 126 workFS := afero.NewBasePathFs(osFS, workDir) 127 128 var fs appfs.FileServer 129 if man.FromAppsDir { 130 fs = app.FSForAppDir(man.Slug()) 131 } else { 132 fs = app.AppsFileServer(i) 133 } 134 src, err := fs.Open(man.Slug(), man.Version(), man.Checksum(), path.Join("/", service.File)) 135 if err != nil { 136 return 137 } 138 defer src.Close() 139 140 dst, err := workFS.OpenFile("index.js", os.O_CREATE|os.O_WRONLY, 0640) 141 if err != nil { 142 return 143 } 144 defer dst.Close() 145 146 _, err = io.Copy(dst, src) 147 if err != nil { 148 return 149 } 150 151 return workDir, cleanDir, nil 152 } 153 154 func (w *serviceWorker) Slug() string { 155 return w.slug 156 } 157 158 func (w *serviceWorker) PrepareCmdEnv(ctx *job.TaskContext, i *instance.Instance) (cmd string, env []string, err error) { 159 type serviceEvent struct { 160 Doc interface{} `json:"doc"` 161 } 162 163 var doc serviceEvent 164 marshaled := []byte{} 165 if err := ctx.UnmarshalEvent(&doc); err == nil { 166 marshaled, err = json.Marshal(doc.Doc) 167 if err != nil { 168 return "", nil, err 169 } 170 } 171 172 payload, err := preparePayload(ctx, w.workDir) 173 if err != nil { 174 return "", nil, err 175 } 176 177 token := i.BuildAppToken(w.man.Slug(), "") 178 cmd = config.GetConfig().Konnectors.Cmd 179 env = []string{ 180 "COZY_URL=" + i.PageURL("/", nil), 181 "COZY_CREDENTIALS=" + token, 182 "COZY_LANGUAGE=node", // default to node language for services 183 "COZY_LOCALE=" + i.Locale, 184 "COZY_TIME_LIMIT=" + ctxToTimeLimit(ctx), 185 "COZY_JOB_ID=" + ctx.ID(), 186 "COZY_COUCH_DOC=" + string(marshaled), 187 "COZY_PAYLOAD=" + payload, 188 "COZY_FIELDS=" + string(w.fields), 189 } 190 if triggerID, ok := ctx.TriggerID(); ok { 191 env = append(env, "COZY_TRIGGER_ID="+triggerID) 192 } 193 return 194 } 195 196 func (w *serviceWorker) Logger(ctx *job.TaskContext) logger.Logger { 197 log := ctx.Logger().WithField("slug", w.Slug()) 198 if w.name != "" { 199 log = log.WithField("name", w.name) 200 } 201 return log 202 } 203 204 func (w *serviceWorker) ScanOutput(ctx *job.TaskContext, i *instance.Instance, line []byte) error { 205 var msg struct { 206 Type string `json:"type"` 207 Message string `json:"message"` 208 } 209 if err := json.Unmarshal(line, &msg); err != nil { 210 return fmt.Errorf("Could not parse stdout as JSON: %q", string(line)) 211 } 212 213 // Truncate very long messages 214 if len(msg.Message) > 4000 { 215 msg.Message = msg.Message[:4000] 216 } 217 218 log := w.Logger(ctx) 219 switch msg.Type { 220 case konnectorMsgTypeDebug, konnectorMsgTypeInfo: 221 log.Debug(msg.Message) 222 case konnectorMsgTypeWarning, "warn": 223 log.Warn(msg.Message) 224 case konnectorMsgTypeError: 225 log.Error(msg.Message) 226 case konnectorMsgTypeCritical: 227 log.Error(msg.Message) 228 } 229 return nil 230 } 231 232 func (w *serviceWorker) Error(i *instance.Instance, err error) error { 233 return err 234 } 235 236 func (w *serviceWorker) Commit(ctx *job.TaskContext, errjob error) error { 237 log := w.Logger(ctx) 238 if errjob == nil { 239 log.Info("Service success") 240 } else { 241 log.Infof("Service failure: %s", errjob) 242 } 243 return nil 244 }