You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/10/03 21:04:50 UTC
(airavata-custos) 02/04: create vast home directories when a cluster account is provisioned (#590)
This is an automated email from the ASF dual-hosted git repository.
yasithdev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit db64e2a72299f7a8a24c400468c8f7aa98309b95
Author: Yasith Jayawardana <ya...@icloud.com>
AuthorDate: Sat Oct 3 17:04:38 2026 -0400
create vast home directories when a cluster account is provisioned (#590)
---
CONFIG.md | 23 +++++
config/custos.yaml | 23 +++++
connectors/VAST/Storage-Provisioner/README.md | 42 ++++++++
.../Storage-Provisioner/internal/client/client.go | 113 +++++++++++++++++++++
.../internal/client/client_test.go | 58 +++++++++++
.../internal/subscribers/subscriber.go | 100 ++++++++++++++++++
.../internal/subscribers/subscriber_test.go | 71 +++++++++++++
.../VAST/Storage-Provisioner/pkg/vast/loader.go | 84 +++++++++++++++
internal/connectors/loader.go | 2 +
pkg/service/compute_cluster_user.go | 8 +-
10 files changed, 523 insertions(+), 1 deletion(-)
diff --git a/CONFIG.md b/CONFIG.md
index 91915a61e..549e12d25 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -132,6 +132,28 @@ connectors:
http_timeout: "30s"
```
+#### VAST Storage Provisioner
+
+```yaml
+ vast-storage:
+ type: "vast-storage-provisioner"
+ enabled: true
+ custos_cluster_id: "cluster-001"
+ vms:
+ url: "http://vms.example.org"
+ username: "custos"
+ password: "${VAST_API_PASSWORD}"
+ tenant_id: 1
+ # config/custos.yaml holds the full list.
+ storage:
+ - path: "/home/{user}"
+ owner: "{user}"
+ group: "{user}"
+ mode: "0700"
+ hard_limit: 107374182400 # 100 GiB
+ hard_limit_inodes: 1000000
+```
+
#### AMIE Processor
```yaml
@@ -223,6 +245,7 @@ When a connector is disabled:
- `slurm-association-mapper` - SLURM Association Mapper
- `slurm-usage-monitor` - SLURM Usage Monitor
- `comanage-identity-provisioner` - COmanage Identity Provisioner
+- `vast-storage-provisioner` - VAST Storage Provisioner
- `amie-processor` - AMIE Processor
New connector types can be added by:
diff --git a/config/custos.yaml b/config/custos.yaml
index 53684f48e..f88804459 100644
--- a/config/custos.yaml
+++ b/config/custos.yaml
@@ -77,6 +77,29 @@ connectors:
homedir_prefix: "/home/"
http_timeout: "30s"
+ vast-storage:
+ type: "vast-storage-provisioner"
+ enabled: false
+ custos_cluster_id: "${CUSTOS_CLUSTER_ID}"
+ vms:
+ url: "${VAST_VMS_URL}"
+ username: "${VAST_API_USER}"
+ password: "${VAST_API_PASSWORD}"
+ tenant_id: ${VAST_TENANT_ID}
+ storage:
+ - path: "/home/{user}"
+ owner: "{user}"
+ group: "{user}"
+ mode: "0700"
+ hard_limit: 107374182400 # 100 GiB
+ hard_limit_inodes: 1000000
+ - path: "/scratch/{user}"
+ owner: "{user}"
+ group: "{user}"
+ mode: "0700"
+ hard_limit: 10995116277760 # 10 TiB
+ hard_limit_inodes: 10000000
+
amie-processor:
type: "amie-processor"
enabled: true
diff --git a/connectors/VAST/Storage-Provisioner/README.md b/connectors/VAST/Storage-Provisioner/README.md
new file mode 100644
index 000000000..cf2f7c3c0
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/README.md
@@ -0,0 +1,42 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# VAST Storage-Provisioner
+
+Creates directories and quotas on a VAST cluster through the VMS REST API, once the COmanage Identity-Provisioner
+on the same Custos cluster has provisioned the account. Nothing is changed once it exists, and nothing is deleted.
+
+The directories come from the `storage` list in the config. Each entry's `path`, `owner` and `group` are templates
+over `{user}`, the account's username. Entries are created in config order, with a quota named after the path.
+
+| Trigger | Runs |
+|---|---|
+| `compute_cluster_user::update` with `provisioned_at` set | The account's directories |
+| Startup, then every 24 hours | The same, for every account on the cluster |
+
+Configuration is in [`CONFIG.md`](../../../CONFIG.md#vast-storage-provisioner).
+
+## Prerequisites on VMS
+
+VAST admins make these changes before the connector is enabled.
+
+| Prerequisite | Why |
+|---|---|
+| The COmanage LDAP is a VMS user-directory provider | VMS resolves owners only from its providers; Custos users are otherwise unknown and every create retries |
+| The service account may create folders and quotas | The account the connector logs in with |
diff --git a/connectors/VAST/Storage-Provisioner/internal/client/client.go b/connectors/VAST/Storage-Provisioner/internal/client/client.go
new file mode 100644
index 000000000..daa480472
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/internal/client/client.go
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package client creates folders and quotas through the VAST VMS REST API.
+// Each create logs in for a fresh JWT from `/api/token/`, since creates are rare.
+// VMS answers most refusals, including an existing folder, with a 503, so a
+// refused create counts as success when the folder or quota exists; any other
+// error goes back to the bus, which retries it.
+package client
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+type Client struct {
+ url, username, password string
+ tenantID int
+ http *http.Client
+}
+
+func New(vmsURL, username, password string, tenantID int) *Client {
+ return &Client{
+ url: strings.TrimSuffix(vmsURL, "/") + "/api/",
+ username: username,
+ password: password,
+ tenantID: tenantID,
+ http: &http.Client{Timeout: 10 * time.Second},
+ }
+}
+
+// CreateFolder creates path owned by user:group unless it exists.
+func (c *Client) CreateFolder(ctx context.Context, path, user, group string, mode int) error {
+ return c.create(ctx, "folders/create_folder/", map[string]any{
+ "path": path, "user": user, "group": group, "owner_is_group": false, "create_dir_mode": fmt.Sprintf("%o", mode), "tenant_id": c.tenantID,
+ }, func(token string) bool {
+ return c.do(ctx, token, http.MethodPost, "folders/stat_path/", map[string]any{"path": path, "tenant_id": c.tenantID}, nil) == nil
+ })
+}
+
+// CreateQuota creates a quota on path, named after it, unless a quota by that name exists.
+func (c *Client) CreateQuota(ctx context.Context, path string, hardLimit, hardLimitInodes int64) error {
+ return c.create(ctx, "quotas/", map[string]any{
+ "name": path, "path": path, "create_dir": false, "hard_limit": hardLimit, "hard_limit_inodes": hardLimitInodes, "tenant_id": c.tenantID,
+ }, func(token string) bool {
+ var found []json.RawMessage
+ return c.do(ctx, token, http.MethodGet, "quotas/?name="+url.QueryEscape(path), nil, &found) == nil && len(found) > 0
+ })
+}
+
+func (c *Client) create(ctx context.Context, path string, body map[string]any, exists func(token string) bool) error {
+ var t struct{ Access string }
+ if err := c.do(ctx, "", http.MethodPost, "token/", map[string]string{"username": c.username, "password": c.password}, &t); err != nil {
+ return err
+ }
+ if err := c.do(ctx, t.Access, http.MethodPost, path, body, nil); err != nil && !exists(t.Access) {
+ return err
+ }
+ return nil
+}
+
+func (c *Client) do(ctx context.Context, token, method, path string, body, out any) error {
+ var rdr io.Reader
+ if body != nil {
+ b, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+ rdr = bytes.NewReader(b)
+ }
+ req, err := http.NewRequestWithContext(ctx, method, c.url+path, rdr)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ resp, err := c.http.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 300 {
+ msg, _ := io.ReadAll(io.LimitReader(resp.Body, 300))
+ return fmt.Errorf("vast: %s %s: %d: %s", method, path, resp.StatusCode, msg)
+ }
+ if out == nil {
+ return nil
+ }
+ return json.NewDecoder(resp.Body).Decode(out)
+}
diff --git a/connectors/VAST/Storage-Provisioner/internal/client/client_test.go b/connectors/VAST/Storage-Provisioner/internal/client/client_test.go
new file mode 100644
index 000000000..ae01cbe58
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/internal/client/client_test.go
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package client
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestCreateFolder(t *testing.T) {
+ tests := []struct {
+ name string
+ create, stat int
+ wantErr bool
+ }{
+ {name: "created", create: 200},
+ {name: "already exists", create: 503, stat: 200},
+ {name: "unknown owner", create: 503, stat: 503, wantErr: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case r.URL.Path == "/api/token/":
+ _, _ = w.Write([]byte(`{"access":"fresh"}`))
+ case r.Header.Get("Authorization") != "Bearer fresh":
+ w.WriteHeader(http.StatusForbidden)
+ case r.URL.Path == "/api/folders/stat_path/":
+ w.WriteHeader(tt.stat)
+ default:
+ w.WriteHeader(tt.create)
+ }
+ }))
+ defer srv.Close()
+ err := New(srv.URL, "u", "p", 1).CreateFolder(context.Background(), "/home/custos-jdoe", "custos-jdoe", "custos-jdoe", 0o700)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("err=%v", err)
+ }
+ })
+ }
+}
diff --git a/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber.go b/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber.go
new file mode 100644
index 000000000..046cb3261
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber.go
@@ -0,0 +1,100 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package subscribers creates the configured directories and quotas on VAST
+// once COmanage has provisioned the account, since VAST resolves owners by name
+// from the LDAP that COmanage feeds. Mounts are created in config order, so
+// parents come first.
+package subscribers
+
+import (
+ "context"
+ "errors"
+ "log/slog"
+ "strings"
+ "time"
+
+ "github.com/apache/airavata-custos/connectors/VAST/Storage-Provisioner/internal/client"
+ "github.com/apache/airavata-custos/pkg/events"
+ "github.com/apache/airavata-custos/pkg/models"
+ "github.com/apache/airavata-custos/pkg/service"
+)
+
+// Mount is a directory created for each account. Path, Owner and Group are
+// templates over {user}, the account's username; its quota is named after the path.
+type Mount struct {
+ Path, Owner, Group string
+ Mode int
+ HardLimit, HardLimitInodes int64
+}
+
+type StorageSubscriber struct {
+ vms *client.Client
+ bus *events.Bus
+ core service.CoreService
+ custosClusterID string
+ mounts []Mount
+}
+
+func NewStorageSubscriber(vms *client.Client, bus *events.Bus, core service.CoreService, custosClusterID string, mounts []Mount) *StorageSubscriber {
+ return &StorageSubscriber{vms: vms, bus: bus, core: core, custosClusterID: custosClusterID, mounts: mounts}
+}
+
+// RegisterSubscribers waits for the "provisioned" update COmanage publishes, which alone releases the work.
+func (s *StorageSubscriber) RegisterSubscribers(subscriber string) {
+ s.bus.SubscribeComputeClusterUserUpdated(subscriber, s.ensureUser)
+}
+
+// ensureUser needs no approval check: COmanage provisions approved accounts only.
+func (s *StorageSubscriber) ensureUser(ctx context.Context, cu models.ComputeClusterUser) error {
+ if cu.ComputeClusterID != s.custosClusterID || cu.ProvisionedAt == nil {
+ return nil
+ }
+ r := strings.NewReplacer("{user}", cu.LocalUsername)
+ for _, m := range s.mounts {
+ path := r.Replace(m.Path)
+ if err := s.vms.CreateFolder(ctx, path, r.Replace(m.Owner), r.Replace(m.Group), m.Mode); err != nil {
+ return err
+ }
+ if err := s.vms.CreateQuota(ctx, path, m.HardLimit, m.HardLimitInodes); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *StorageSubscriber) StartReconciler(ctx context.Context) {
+ for {
+ s.reconcile(ctx)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(24 * time.Hour):
+ }
+ }
+}
+
+func (s *StorageSubscriber) reconcile(ctx context.Context) {
+ users, err := s.core.ListComputeClusterUsersByCluster(ctx, s.custosClusterID)
+ errs := []error{err}
+ for _, cu := range users {
+ errs = append(errs, s.ensureUser(ctx, cu))
+ }
+ if err := errors.Join(errs...); err != nil {
+ slog.Error("VAST reconciler", "err", err)
+ }
+}
diff --git a/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber_test.go b/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber_test.go
new file mode 100644
index 000000000..efd6e6a36
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/internal/subscribers/subscriber_test.go
@@ -0,0 +1,71 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package subscribers
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/apache/airavata-custos/connectors/VAST/Storage-Provisioner/internal/client"
+ "github.com/apache/airavata-custos/pkg/models"
+)
+
+var layout = []Mount{
+ {Path: "/home/{user}", Owner: "{user}", Group: "{user}", Mode: 0o700, HardLimit: 1, HardLimitInodes: 1},
+ {Path: "/scratch/{user}", Owner: "{user}", Group: "{user}", Mode: 0o700, HardLimit: 1, HardLimitInodes: 1},
+}
+
+func TestEnsureUser(t *testing.T) {
+ now := time.Now()
+ tests := []struct {
+ name string
+ provisionedAt *time.Time
+ want []string
+ }{
+ {name: "creates in order", provisionedAt: &now, want: []string{"folder /home/custos-jdoe", "quota /home/custos-jdoe", "folder /scratch/custos-jdoe", "quota /scratch/custos-jdoe"}},
+ {name: "skips unprovisioned"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var log []string
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var body struct{ Path string }
+ _ = json.NewDecoder(r.Body).Decode(&body)
+ switch r.URL.Path {
+ case "/api/token/":
+ _, _ = w.Write([]byte(`{"access":"t"}`))
+ case "/api/folders/create_folder/":
+ log = append(log, "folder "+body.Path)
+ case "/api/quotas/":
+ log = append(log, "quota "+body.Path)
+ }
+ }))
+ defer srv.Close()
+ s := NewStorageSubscriber(client.New(srv.URL, "u", "p", 1), nil, nil, "c1", layout)
+ cu := models.ComputeClusterUser{ComputeClusterID: "c1", LocalUsername: "custos-jdoe", ProvisionedAt: tt.provisionedAt}
+ if err := s.ensureUser(context.Background(), cu); err != nil || !slices.Equal(log, tt.want) {
+ t.Fatalf("err=%v calls=%v, want %v", err, log, tt.want)
+ }
+ })
+ }
+}
diff --git a/connectors/VAST/Storage-Provisioner/pkg/vast/loader.go b/connectors/VAST/Storage-Provisioner/pkg/vast/loader.go
new file mode 100644
index 000000000..7d361677a
--- /dev/null
+++ b/connectors/VAST/Storage-Provisioner/pkg/vast/loader.go
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package vast is the VAST Storage-Provisioner entry point. Wired from
+// internal/connectors/loader.go.
+package vast
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "strconv"
+ "sync"
+
+ "github.com/jmoiron/sqlx"
+
+ "github.com/apache/airavata-custos/connectors/VAST/Storage-Provisioner/internal/client"
+ "github.com/apache/airavata-custos/connectors/VAST/Storage-Provisioner/internal/subscribers"
+ "github.com/apache/airavata-custos/internal/config"
+ "github.com/apache/airavata-custos/pkg/events"
+ "github.com/apache/airavata-custos/pkg/identity"
+ "github.com/apache/airavata-custos/pkg/service"
+)
+
+// Type is the connector type in the config file and the subscriber name on the bus.
+const Type = "vast-storage-provisioner"
+
+func LoadConnector(ctx context.Context, _ *sqlx.DB, eventBus *events.Bus, coreService *service.Service, wg *sync.WaitGroup, _ *identity.Router, connectorConfig *config.ConnectorConfig) error {
+ vms, _ := connectorConfig.GetNestedConfig("vms")
+ vmsURL, _ := vms["url"].(string)
+ username, _ := vms["username"].(string)
+ password, _ := vms["password"].(string)
+ tenantID, _ := vms["tenant_id"].(int)
+ clusterID, _ := connectorConfig.GetStringField("custos_cluster_id")
+ storage, _ := connectorConfig.Config["storage"].([]interface{})
+ if vmsURL == "" || username == "" || password == "" || tenantID == 0 || clusterID == "" || len(storage) == 0 {
+ slog.Info("vast provisioner: required config not set; skipping")
+ return nil
+ }
+ mounts, err := parseMounts(storage)
+ if err != nil {
+ return err
+ }
+ subscriber := subscribers.NewStorageSubscriber(client.New(vmsURL, username, password, tenantID), eventBus, coreService, clusterID, mounts)
+ subscriber.RegisterSubscribers(Type)
+ slog.Info("vast provisioner: subscriber registered", "vms", vmsURL, "cluster_id", clusterID)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ subscriber.StartReconciler(ctx)
+ }()
+ return nil
+}
+
+func parseMounts(storage []interface{}) ([]subscribers.Mount, error) {
+ var mounts []subscribers.Mount
+ for i, e := range storage {
+ m, _ := e.(map[string]interface{})
+ str := func(k string) string { v, _ := m[k].(string); return v }
+ hardLimit, _ := m["hard_limit"].(int)
+ hardLimitInodes, _ := m["hard_limit_inodes"].(int)
+ mode, err := strconv.ParseInt(str("mode"), 8, 32)
+ if err != nil {
+ return nil, fmt.Errorf("vast provisioner: storage[%d].mode must be octal, such as \"0700\": %w", i, err)
+ }
+ mounts = append(mounts, subscribers.Mount{Path: str("path"), Owner: str("owner"), Group: str("group"),
+ Mode: int(mode), HardLimit: int64(hardLimit), HardLimitInodes: int64(hardLimitInodes)})
+ }
+ return mounts, nil
+}
diff --git a/internal/connectors/loader.go b/internal/connectors/loader.go
index 562c0aa37..09fad4663 100644
--- a/internal/connectors/loader.go
+++ b/internal/connectors/loader.go
@@ -28,6 +28,7 @@ import (
"github.com/apache/airavata-custos/connectors/SLURM/Association-Mapper/pkg/smapper"
"github.com/apache/airavata-custos/connectors/SLURM/Usage-Monitor/pkg/monitor"
"github.com/apache/airavata-custos/connectors/TempAccount/pkg/tempaccount"
+ "github.com/apache/airavata-custos/connectors/VAST/Storage-Provisioner/pkg/vast"
"github.com/apache/airavata-custos/internal/config"
"github.com/apache/airavata-custos/pkg/events"
"github.com/apache/airavata-custos/pkg/identity"
@@ -45,6 +46,7 @@ func LoadConnectorsFromConfig(ctx context.Context, cfg *config.Config, database
monitor.Type: monitor.LoadConnector,
tempaccount.Type: tempaccount.LoadConnector,
analytics.Type: analytics.LoadConnector,
+ vast.Type: vast.LoadConnector,
}
for connectorName, connectorCfg := range cfg.Connectors {
diff --git a/pkg/service/compute_cluster_user.go b/pkg/service/compute_cluster_user.go
index 4c921f790..1a773edfb 100644
--- a/pkg/service/compute_cluster_user.go
+++ b/pkg/service/compute_cluster_user.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
"fmt"
"strings"
+ "time"
"github.com/apache/airavata-custos/internal/store"
"github.com/apache/airavata-custos/pkg/events"
@@ -382,7 +383,12 @@ func (s *Service) MarkComputeClusterUserProvisioned(ctx context.Context, id stri
return ErrNotFound
}
if err := s.inTx(ctx, func(tx *sql.Tx) error {
- return s.clusterUsers.MarkProvisioned(ctx, tx, id)
+ if err := s.clusterUsers.MarkProvisioned(ctx, tx, id); err != nil {
+ return err
+ }
+ now := time.Now()
+ cu.ProvisionedAt = &now
+ return s.eventBus.Publish(ctx, tx, events.ComputeClusterUserUpdateEvent, cu)
}); err != nil {
return fmt.Errorf("mark compute cluster user provisioned: %w", err)
}