1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
15 "git.arvados.org/arvados.git/lib/controller/railsproxy"
16 "git.arvados.org/arvados.git/lib/controller/rpc"
17 "git.arvados.org/arvados.git/sdk/go/arvados"
18 "git.arvados.org/arvados.git/sdk/go/ctxlog"
19 "git.arvados.org/arvados.git/sdk/go/httpserver"
20 "github.com/fsnotify/fsnotify"
21 "github.com/sirupsen/logrus"
24 type railsProxy = rpc.Conn
27 cluster *arvados.Cluster
28 *railsProxy // handles API methods that aren't defined on Conn itself
29 vocabularyCache *arvados.Vocabulary
34 func NewConn(cluster *arvados.Cluster) *Conn {
35 railsProxy := railsproxy.NewConn(cluster)
36 railsProxy.RedactHostInErrors = true
39 railsProxy: railsProxy,
41 conn.loginController = chooseLoginController(cluster, &conn)
45 func (conn *Conn) checkProperties(ctx context.Context, properties interface{}) error {
46 if properties == nil {
49 var props map[string]interface{}
50 switch properties := properties.(type) {
52 err := json.Unmarshal([]byte(properties), &props)
56 case map[string]interface{}:
59 return fmt.Errorf("unexpected properties type %T", properties)
61 voc, err := conn.VocabularyGet(ctx)
65 err = voc.Check(props)
67 return httpErrorf(http.StatusBadRequest, voc.Check(props).Error())
72 func watchVocabulary(logger logrus.FieldLogger, vocPath string, fn func()) {
73 watcher, err := fsnotify.NewWatcher()
75 logger.WithError(err).Error("vocabulary fsnotify setup failed")
80 err = watcher.Add(vocPath)
82 logger.WithError(err).Error("vocabulary file watcher failed")
88 case err, ok := <-watcher.Errors:
92 logger.WithError(err).Warn("vocabulary file watcher error")
93 case _, ok := <-watcher.Events:
97 for len(watcher.Events) > 0 {
105 func (conn *Conn) loadVocabularyFile() error {
106 vf, err := os.ReadFile(conn.cluster.API.VocabularyPath)
108 return fmt.Errorf("couldn't read vocabulary file %q: %v", conn.cluster.API.VocabularyPath, err)
110 mk := make([]string, 0, len(conn.cluster.Collections.ManagedProperties))
111 for k := range conn.cluster.Collections.ManagedProperties {
114 voc, err := arvados.NewVocabulary(vf, mk)
116 return fmt.Errorf("while loading vocabulary file %q: %s", conn.cluster.API.VocabularyPath, err)
120 return fmt.Errorf("while validating vocabulary file %q: %s", conn.cluster.API.VocabularyPath, err)
122 conn.vocabularyCache = voc
126 // VocabularyGet refreshes the vocabulary cache if necessary and returns it.
127 func (conn *Conn) VocabularyGet(ctx context.Context) (arvados.Vocabulary, error) {
128 if conn.cluster.API.VocabularyPath == "" {
129 return arvados.Vocabulary{
130 Tags: map[string]arvados.VocabularyTag{},
133 logger := ctxlog.FromContext(ctx)
134 if conn.vocabularyCache == nil {
135 // Initial load of vocabulary file.
136 err := conn.loadVocabularyFile()
138 logger.WithError(err).Error("error loading vocabulary file")
139 return arvados.Vocabulary{
140 Tags: map[string]arvados.VocabularyTag{},
143 go watchVocabulary(logger, conn.cluster.API.VocabularyPath, func() {
144 logger.Info("vocabulary file changed, it'll be reloaded next time it's needed")
145 conn.reloadVocabulary = true
147 } else if conn.reloadVocabulary {
148 // Requested reload of vocabulary file.
149 conn.reloadVocabulary = false
150 err := conn.loadVocabularyFile()
152 logger.WithError(err).Error("error reloading vocabulary file - ignoring")
154 logger.Info("vocabulary file reloaded successfully")
157 return *conn.vocabularyCache, nil
160 // Logout handles the logout of conn giving to the appropriate loginController
161 func (conn *Conn) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
162 return conn.loginController.Logout(ctx, opts)
165 // Login handles the login of conn giving to the appropriate loginController
166 func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
167 return conn.loginController.Login(ctx, opts)
170 // UserAuthenticate handles the User Authentication of conn giving to the appropriate loginController
171 func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
172 return conn.loginController.UserAuthenticate(ctx, opts)
175 func (conn *Conn) GroupContents(ctx context.Context, options arvados.GroupContentsOptions) (arvados.ObjectList, error) {
176 // The requested UUID can be a user (virtual home project), which we just pass on to
178 if strings.Index(options.UUID, "-j7d0g-") != 5 {
179 return conn.railsProxy.GroupContents(ctx, options)
182 var resp arvados.ObjectList
184 // Get the group object
185 respGroup, err := conn.GroupGet(ctx, arvados.GetOptions{UUID: options.UUID})
190 // If the group has groupClass 'filter', apply the filters before getting the contents.
191 if respGroup.GroupClass == "filter" {
192 if filters, ok := respGroup.Properties["filters"].([]interface{}); ok {
193 for _, f := range filters {
194 // f is supposed to be a []string
195 tmp, ok2 := f.([]interface{})
196 if !ok2 || len(tmp) < 3 {
197 return resp, fmt.Errorf("filter unparsable: %T, %+v, original field: %T, %+v\n", tmp, tmp, f, f)
199 var filter arvados.Filter
200 if attr, ok2 := tmp[0].(string); ok2 {
203 return resp, fmt.Errorf("filter unparsable: attribute must be string: %T, %+v, filter: %T, %+v\n", tmp[0], tmp[0], f, f)
205 if operator, ok2 := tmp[1].(string); ok2 {
206 filter.Operator = operator
208 return resp, fmt.Errorf("filter unparsable: operator must be string: %T, %+v, filter: %T, %+v\n", tmp[1], tmp[1], f, f)
210 filter.Operand = tmp[2]
211 options.Filters = append(options.Filters, filter)
214 return resp, fmt.Errorf("filter unparsable: not an array\n")
216 // Use the generic /groups/contents endpoint for filter groups
220 return conn.railsProxy.GroupContents(ctx, options)
223 func httpErrorf(code int, format string, args ...interface{}) error {
224 return httpserver.ErrorWithStatus(fmt.Errorf(format, args...), code)