From 3e55ab194999a075ba41ee13caa3d9eeee005aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= Date: Fri, 5 Aug 2022 16:31:01 +0200 Subject: [PATCH] feat: separate state per cloud (#395) --- api/aws/local.go | 12 ++-- api/deploy/deploy.go | 40 +++++++------- api/server.go | 47 +++++++++------- api/services/config_prefix.go | 37 +++++++++++++ api/services/service.go | 55 ++++++++++--------- db/database.go | 20 ++----- db/interface.go | 6 +- db/local_database.go | 12 +--- db/local_lock_db.go | 7 ++- db/lock_db.go | 16 +++--- db/user_config_storage.go | 12 ++-- resources/common/resource_id.go | 23 ++++++++ resources/resource_metadata.go | 24 +++++--- resources/resource_with_id.go | 8 ++- resources/types/kubernetes_node_pool.go | 5 ++ ...rk_interface_security_group_association.go | 6 ++ resources/types/object_storage_object.go | 6 ++ resources/types/route_table.go | 6 ++ resources/types/route_table_association.go | 6 ++ resources/types/subnet.go | 6 ++ resources/types/vault_access_policy.go | 6 ++ resources/types/vault_secret.go | 6 ++ test/deploy/deploy_test.go | 10 ++-- 23 files changed, 247 insertions(+), 129 deletions(-) create mode 100644 api/services/config_prefix.go create mode 100644 resources/common/resource_id.go diff --git a/api/aws/local.go b/api/aws/local.go index baeec4eb..6be5ce86 100644 --- a/api/aws/local.go +++ b/api/aws/local.go @@ -13,8 +13,8 @@ func newLocalClient() (*LocalClient, error) { return &LocalClient{}, nil } -func (c LocalClient) SaveFile(userId string, fileName string, content string) error { - filePath, err := c.getFilePath(userId, fileName) +func (c LocalClient) SaveFile(configPrefix string, fileName string, content string) error { + filePath, err := c.getFilePath(configPrefix, fileName) if err != nil { return err } @@ -26,8 +26,8 @@ func (c LocalClient) SaveFile(userId string, fileName string, content string) er return nil } -func (c LocalClient) ReadFile(userId string, fileName string) (string, error) { - filePath, err := c.getFilePath(userId, fileName) +func (c LocalClient) ReadFile(configPrefix string, fileName string) (string, error) { + filePath, err := c.getFilePath(configPrefix, fileName) if err != nil { return "", err } @@ -42,8 +42,8 @@ func (c LocalClient) ReadFile(userId string, fileName string) (string, error) { return string(file), nil } -func (c LocalClient) getFilePath(userId string, fileName string) (string, error) { - tmpDir := path.Join(os.TempDir(), "multy", userId, "local") +func (c LocalClient) getFilePath(configPrefix string, fileName string) (string, error) { + tmpDir := path.Join(os.TempDir(), "multy", configPrefix, "local") err := os.MkdirAll(tmpDir, os.ModeDir|(os.ModePerm&0775)) if err != nil { return "", err diff --git a/api/deploy/deploy.go b/api/deploy/deploy.go index 7890640e..dfe1aca2 100644 --- a/api/deploy/deploy.go +++ b/api/deploy/deploy.go @@ -41,14 +41,14 @@ func NewDeploymentExecutor() DeploymentExecutor { return DeploymentExecutor{TfCmd: terraformCmd{}} } -func (d DeploymentExecutor) Deploy(ctx context.Context, c *resources.MultyConfig, prev resources.Resource, curr resources.Resource) (rollbackFn func(), err error) { - tmpDir := GetTempDirForUser(c.GetUserId()) - encoded, err := d.EncodeAndStoreTfFile(ctx, c, prev, curr) +func (d DeploymentExecutor) Deploy(ctx context.Context, c *resources.MultyConfig, prev resources.Resource, curr resources.Resource, configPrefix string) (rollbackFn func(), err error) { + tmpDir := GetTempDirForUser(configPrefix) + encoded, err := d.EncodeAndStoreTfFile(ctx, c, prev, curr, configPrefix) if err != nil { return } - err = d.MaybeInit(ctx, c.GetUserId()) + err = d.MaybeInit(ctx, configPrefix) if err != nil { return } @@ -68,7 +68,7 @@ func (d DeploymentExecutor) Deploy(ctx context.Context, c *resources.MultyConfig log.Printf("[ERROR] Rollback unsuccessful: %s\n", err2) return } - _, err2 = d.EncodeAndStoreTfFile(ctx, originalC, curr, prev) + _, err2 = d.EncodeAndStoreTfFile(ctx, originalC, curr, prev, configPrefix) if err2 != nil { log.Printf("[ERROR] Rollback unsuccessful: %s\n", err2) return @@ -96,7 +96,7 @@ func (d DeploymentExecutor) Deploy(ctx context.Context, c *resources.MultyConfig return } -func (d DeploymentExecutor) EncodeAndStoreTfFile(ctx context.Context, c *resources.MultyConfig, prev resources.Resource, curr resources.Resource) (EncodedResources, error) { +func (d DeploymentExecutor) EncodeAndStoreTfFile(ctx context.Context, c *resources.MultyConfig, prev resources.Resource, curr resources.Resource, configPrefix string) (EncodedResources, error) { credentials, err := util.ExtractCloudCredentials(ctx) if err != nil { return EncodedResources{}, err @@ -106,7 +106,7 @@ func (d DeploymentExecutor) EncodeAndStoreTfFile(ctx context.Context, c *resourc return encoded, err } - tfBlock, err := GetTerraformBlock(c.GetUserId()) + tfBlock, err := GetTerraformBlock(configPrefix) if err != nil { return encoded, err } @@ -114,7 +114,7 @@ func (d DeploymentExecutor) EncodeAndStoreTfFile(ctx context.Context, c *resourc // TODO: move this to a proper place hclOutput := tfBlock + encoded.HclString - tmpDir := GetTempDirForUser(c.GetUserId()) + tmpDir := GetTempDirForUser(configPrefix) err = os.MkdirAll(tmpDir, os.ModeDir|(os.ModePerm&0775)) if err != nil { return EncodedResources{}, err @@ -123,8 +123,8 @@ func (d DeploymentExecutor) EncodeAndStoreTfFile(ctx context.Context, c *resourc return encoded, err } -func (d DeploymentExecutor) MaybeInit(ctx context.Context, userId string) error { - tmpDir := GetTempDirForUser(userId) +func (d DeploymentExecutor) MaybeInit(ctx context.Context, configPrefix string) error { + tmpDir := GetTempDirForUser(configPrefix) _, err := os.Stat(filepath.Join(tmpDir, tfDir)) if os.IsNotExist(err) { start := time.Now() @@ -147,17 +147,17 @@ func (d DeploymentExecutor) MaybeInit(ctx context.Context, userId string) error return nil } -func (d DeploymentExecutor) GetState(ctx context.Context, userId string, client db.TfStateReader) (*output.TfState, error) { - return d.TfCmd.GetState(ctx, userId, client) +func (d DeploymentExecutor) GetState(ctx context.Context, configPrefix string, client db.TfStateReader) (*output.TfState, error) { + return d.TfCmd.GetState(ctx, configPrefix, client) } -func (d DeploymentExecutor) RefreshState(ctx context.Context, userId string, c *resources.MultyConfig) error { - _, err := d.EncodeAndStoreTfFile(ctx, c, nil, nil) +func (d DeploymentExecutor) RefreshState(ctx context.Context, configPrefix string, c *resources.MultyConfig) error { + _, err := d.EncodeAndStoreTfFile(ctx, c, nil, nil, configPrefix) if err != nil { return err } - err = d.MaybeInit(ctx, userId) + err = d.MaybeInit(ctx, configPrefix) if err != nil { return err } @@ -167,21 +167,21 @@ func (d DeploymentExecutor) RefreshState(ctx context.Context, userId string, c * log.Printf("[DEBUG] refresh finished in %s", time.Since(start)) }() - return d.refresh(ctx, userId) + return d.refresh(ctx, configPrefix) } -func (d DeploymentExecutor) refresh(ctx context.Context, userId string) error { +func (d DeploymentExecutor) refresh(ctx context.Context, configPrefix string) error { start := time.Now() defer func() { log.Printf("[DEBUG] refresh finished in %s", time.Since(start)) }() - tmpDir := GetTempDirForUser(userId) + tmpDir := GetTempDirForUser(configPrefix) return d.TfCmd.Refresh(ctx, tmpDir) } -func GetTempDirForUser(userId string) string { - tmpDir := filepath.Join(os.TempDir(), "multy", userId) +func GetTempDirForUser(configPrefix string) string { + tmpDir := filepath.Join(os.TempDir(), "multy", configPrefix) if flags.Environment == flags.Local { tmpDir = filepath.Join(tmpDir, "local") diff --git a/api/server.go b/api/server.go index fd267ad7..547f76cf 100644 --- a/api/server.go +++ b/api/server.go @@ -373,13 +373,14 @@ func (s *Server) refresh(ctx context.Context, _ *commonpb.Empty) (*commonpb.Empt return nil, err } - lock, err := s.Database.LockConfig(ctx, userId) + lock, err := s.Database.LockConfig(ctx, userId, userId) if err != nil { return nil, err } defer s.Database.UnlockConfig(ctx, lock) - c, err := s.Database.LoadUserConfig(ctx, userId, lock) + // TODO: ask for cloud in request + c, err := s.Database.LoadUserConfig(ctx, userId, userId, lock) if err != nil { return nil, err } @@ -393,7 +394,7 @@ func (s *Server) refresh(ctx context.Context, _ *commonpb.Empty) (*commonpb.Empt return nil, err } - err = s.Database.StoreUserConfig(ctx, c, lock) + err = s.Database.StoreUserConfig(ctx, c, userId, lock) if err != nil { return nil, err } @@ -422,20 +423,27 @@ func (s *Server) list(ctx context.Context, _ *commonpb.Empty) (*commonpb.ListRes return nil, err } - c, err := s.Database.LoadUserConfig(ctx, userId, nil) - if err != nil { - return nil, err - } - resp := &commonpb.ListResourcesResponse{} - for _, r := range c.Resources { - name := string(r.ResourceArgs.ResourceArgs.MessageName().Name()) - name = strings.TrimSuffix(name, "Args") - - resp.Resources = append(resp.Resources, &commonpb.ListResourcesResponse_ResourceMetadata{ - ResourceId: r.ResourceId, - ResourceType: name, - }) + + for cloudValue := range commonpb.CloudProvider_name { + cloud := commonpb.CloudProvider(cloudValue) + if cloud == commonpb.CloudProvider_UNKNOWN_PROVIDER { + continue + } + c, err := s.Database.LoadUserConfig(ctx, userId, services.GetConfigPrefixForCloud(userId, cloud), nil) + if err != nil { + return nil, err + } + + for _, r := range c.Resources { + name := string(r.ResourceArgs.ResourceArgs.MessageName().Name()) + name = strings.TrimSuffix(name, "Args") + + resp.Resources = append(resp.Resources, &commonpb.ListResourcesResponse_ResourceMetadata{ + ResourceId: r.ResourceId, + ResourceType: name, + }) + } } return resp, nil @@ -461,13 +469,14 @@ func (s *Server) deleteResource(ctx context.Context, req *proto.DeleteResourceRe if err != nil { return nil, err } + configPrefix := services.GetConfigPrefix(req, userId) - lock, err := s.Database.LockConfig(ctx, userId) + lock, err := s.Database.LockConfig(ctx, userId, configPrefix) if err != nil { return nil, err } defer s.Database.UnlockConfig(ctx, lock) - c, err := s.Database.LoadUserConfig(ctx, userId, lock) + c, err := s.Database.LoadUserConfig(ctx, userId, configPrefix, lock) if err != nil { return nil, err } @@ -477,7 +486,7 @@ func (s *Server) deleteResource(ctx context.Context, req *proto.DeleteResourceRe return nil, err } - err = s.Database.StoreUserConfig(ctx, c, lock) + err = s.Database.StoreUserConfig(ctx, c, configPrefix, lock) if err != nil { return nil, err } diff --git a/api/services/config_prefix.go b/api/services/config_prefix.go new file mode 100644 index 00000000..ca51730c --- /dev/null +++ b/api/services/config_prefix.go @@ -0,0 +1,37 @@ +package services + +import ( + "fmt" + "github.com/multycloud/multy/api/proto/commonpb" + "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" + "github.com/multycloud/multy/resources/types/metadata" + "google.golang.org/protobuf/proto" + "strings" +) + +func GetConfigPrefixForCloud(userId string, cloud commonpb.CloudProvider) string { + return fmt.Sprintf("%s/%s", userId, strings.ToLower(cloud.String())) +} + +func GetConfigPrefix(req WithResourceId, userId string) string { + cloud := common.ParseCloudFromResourceId(req.GetResourceId()) + if cloud == commonpb.CloudProvider_UNKNOWN_PROVIDER { + return userId + } + + return GetConfigPrefixForCloud(userId, cloud) +} + +func getConfigPrefixForCreateReq(r proto.Message, userId string) string { + converter, err := resources.ResourceMetadatas(metadata.Metadatas).GetConverter(proto.MessageName(r)) + if err != nil { + return "" + } + cloud := converter.ParseCloud(r) + if cloud == commonpb.CloudProvider_UNKNOWN_PROVIDER { + return userId + } + + return GetConfigPrefixForCloud(userId, cloud) +} diff --git a/api/services/service.go b/api/services/service.go index d3871c3a..882981e6 100644 --- a/api/services/service.go +++ b/api/services/service.go @@ -63,17 +63,18 @@ func (s Service[Arg, OutT]) create(ctx context.Context, in CreateRequest[Arg]) ( if err != nil { return } + configPrefix := getConfigPrefixForCreateReq(in.GetResource(), userId) go s.ServiceContext.AwsClient.UpdateQPSMetric(userId, s.ResourceName, "create") log.Printf("[INFO] user: %s. create %s", userId, s.ResourceName) - lock, err := s.ServiceContext.LockConfig(ctx, userId) + lock, err := s.ServiceContext.LockConfig(ctx, userId, configPrefix) if err != nil { return } defer s.ServiceContext.UnlockConfig(ctx, lock) - c, err := s.getConfig(ctx, userId, lock) + c, err := s.getConfig(ctx, configPrefix, lock, configPrefix) if err != nil { return } @@ -85,14 +86,14 @@ func (s Service[Arg, OutT]) create(ctx context.Context, in CreateRequest[Arg]) ( defer func() { if err == nil { - err = s.saveConfig(ctx, c, lock) + err = s.saveConfig(ctx, c, lock, configPrefix) } else { log.Println("[DEBUG] Something went wrong, not storing state") } }() log.Printf("[INFO] Deploying %s\n", resource.GetResourceId()) - rollbackFn, err := s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, nil, resource) + rollbackFn, err := s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, nil, resource, configPrefix) if err != nil { return } @@ -102,11 +103,11 @@ func (s Service[Arg, OutT]) create(ctx context.Context, in CreateRequest[Arg]) ( } }() - return s.readFromConfig(ctx, c, &resourcespb.ReadVirtualNetworkRequest{ResourceId: resource.GetResourceId()}) + return s.readFromConfig(ctx, c, &resourcespb.ReadVirtualNetworkRequest{ResourceId: resource.GetResourceId()}, configPrefix) } -func (s Service[Arg, OutT]) getConfig(ctx context.Context, userId string, lock *db.ConfigLock) (*resources.MultyConfig, error) { - c, err := s.ServiceContext.LoadUserConfig(ctx, userId, lock) +func (s Service[Arg, OutT]) getConfig(ctx context.Context, userId string, lock *db.ConfigLock, configPrefix string) (*resources.MultyConfig, error) { + c, err := s.ServiceContext.LoadUserConfig(ctx, userId, configPrefix, lock) if err != nil { return nil, err } @@ -117,13 +118,13 @@ func (s Service[Arg, OutT]) getConfig(ctx context.Context, userId string, lock * return mconfig, err } -func (s Service[Arg, OutT]) saveConfig(ctx context.Context, c *resources.MultyConfig, lock *db.ConfigLock) error { +func (s Service[Arg, OutT]) saveConfig(ctx context.Context, c *resources.MultyConfig, lock *db.ConfigLock, configPrefix string) error { exportedConfig, err := c.ExportConfig() if err != nil { return err } - return s.ServiceContext.StoreUserConfig(ctx, exportedConfig, lock) + return s.ServiceContext.StoreUserConfig(ctx, exportedConfig, configPrefix, lock) } func (s Service[Arg, OutT]) Read(ctx context.Context, in WithResourceId) (out OutT, err error) { @@ -141,37 +142,38 @@ func (s Service[Arg, OutT]) read(ctx context.Context, in WithResourceId) (OutT, if err != nil { return *new(OutT), err } + configPrefix := GetConfigPrefix(in, userId) go s.ServiceContext.AwsClient.UpdateQPSMetric(userId, s.ResourceName, "read") log.Printf("[INFO] user: %s. read %s %s", userId, s.ResourceName, in.GetResourceId()) - lock, err := s.ServiceContext.LockConfig(ctx, userId) + lock, err := s.ServiceContext.LockConfig(ctx, userId, configPrefix) if err != nil { return *new(OutT), err } defer s.ServiceContext.UnlockConfig(ctx, lock) - c, err := s.getConfig(ctx, userId, nil) + c, err := s.getConfig(ctx, configPrefix, lock, configPrefix) if err != nil { return *new(OutT), err } - _, err = s.ServiceContext.DeploymentExecutor.EncodeAndStoreTfFile(ctx, c, nil, nil) + _, err = s.ServiceContext.DeploymentExecutor.EncodeAndStoreTfFile(ctx, c, nil, nil, configPrefix) if err != nil { return *new(OutT), err } - return s.readFromConfig(ctx, c, in) + return s.readFromConfig(ctx, c, in, configPrefix) } -func (s Service[Arg, OutT]) readFromConfig(ctx context.Context, c *resources.MultyConfig, in WithResourceId) (OutT, error) { +func (s Service[Arg, OutT]) readFromConfig(ctx context.Context, c *resources.MultyConfig, in WithResourceId, configPrefix string) (OutT, error) { for _, r := range c.Resources.GetAll() { if r.GetResourceId() == in.GetResourceId() { - err := s.ServiceContext.DeploymentExecutor.MaybeInit(ctx, c.GetUserId()) + err := s.ServiceContext.DeploymentExecutor.MaybeInit(ctx, configPrefix) if err != nil { return *new(OutT), err } - state, err := s.ServiceContext.DeploymentExecutor.GetState(ctx, c.GetUserId(), s.ServiceContext.Database) + state, err := s.ServiceContext.DeploymentExecutor.GetState(ctx, configPrefix, s.ServiceContext.Database) if err != nil { return *new(OutT), err } @@ -204,15 +206,17 @@ func (s Service[Arg, OutT]) update(ctx context.Context, in UpdateRequest[Arg]) ( if err != nil { return } + configPrefix := GetConfigPrefix(in, userId) + go s.ServiceContext.AwsClient.UpdateQPSMetric(userId, s.ResourceName, "update") log.Printf("[INFO] user: %s. update %s %s", userId, s.ResourceName, in.GetResourceId()) - lock, err := s.ServiceContext.LockConfig(ctx, userId) + lock, err := s.ServiceContext.LockConfig(ctx, userId, configPrefix) if err != nil { return } defer s.ServiceContext.UnlockConfig(ctx, lock) - c, err := s.getConfig(ctx, userId, lock) + c, err := s.getConfig(ctx, configPrefix, lock, configPrefix) if err != nil { return } @@ -224,13 +228,13 @@ func (s Service[Arg, OutT]) update(ctx context.Context, in UpdateRequest[Arg]) ( defer func() { if err == nil { - err = s.saveConfig(ctx, c, lock) + err = s.saveConfig(ctx, c, lock, configPrefix) } else { log.Println("[DEBUG] Something went wrong, not storing state") } }() - rollbackFn, err := s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, r, r) + rollbackFn, err := s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, r, r, configPrefix) if err != nil { return } @@ -239,7 +243,7 @@ func (s Service[Arg, OutT]) update(ctx context.Context, in UpdateRequest[Arg]) ( rollbackFn() } }() - return s.readFromConfig(ctx, c, in) + return s.readFromConfig(ctx, c, in, configPrefix) } func (s Service[Arg, OutT]) Delete(ctx context.Context, in WithResourceId) (_ *commonpb.Empty, err error) { @@ -256,14 +260,15 @@ func (s Service[Arg, OutT]) delete(ctx context.Context, in WithResourceId) (out if err != nil { return } + configPrefix := GetConfigPrefix(in, userId) go s.ServiceContext.AwsClient.UpdateQPSMetric(userId, s.ResourceName, "delete") log.Printf("[INFO] user: %s. delete %s %s", userId, s.ResourceName, in.GetResourceId()) - lock, err := s.ServiceContext.LockConfig(ctx, userId) + lock, err := s.ServiceContext.LockConfig(ctx, userId, configPrefix) if err != nil { return } defer s.ServiceContext.UnlockConfig(ctx, lock) - c, err := s.getConfig(ctx, userId, lock) + c, err := s.getConfig(ctx, configPrefix, lock, configPrefix) if err != nil { return } @@ -274,13 +279,13 @@ func (s Service[Arg, OutT]) delete(ctx context.Context, in WithResourceId) (out defer func() { if err == nil { - err = s.saveConfig(ctx, c, lock) + err = s.saveConfig(ctx, c, lock, configPrefix) } else { log.Println("[DEBUG] Something went wrong, not storing state") } }() - _, err = s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, previousResource, nil) + _, err = s.ServiceContext.DeploymentExecutor.Deploy(ctx, c, previousResource, nil, configPrefix) if err != nil { if s, ok := status.FromError(err); ok && s.Code() == codes.InvalidArgument { for _, details := range s.Details() { diff --git a/db/database.go b/db/database.go index 1d756f52..163d0c96 100644 --- a/db/database.go +++ b/db/database.go @@ -15,7 +15,7 @@ import ( type database struct { *userConfigStorage - lockDatabase *RemoteLockDatabase + *RemoteLockDatabase sqlConnection *sql.DB AwsClient aws_client.AwsClient } @@ -83,14 +83,6 @@ func (d *database) CreateUser(ctx context.Context, emailAddress string) (apiKey return apiKey, err } -func (d *database) LockConfig(ctx context.Context, userId string) (lock *ConfigLock, err error) { - return d.lockDatabase.LockConfig(ctx, userId) -} - -func (d *database) UnlockConfig(ctx context.Context, lock *ConfigLock) error { - return d.lockDatabase.UnlockConfig(ctx, lock) -} - func (d *database) LoadTerraformState(ctx context.Context, userId string) (string, error) { result, err := d.AwsClient.ReadFile(userId, TfState) if err != nil { @@ -114,14 +106,14 @@ func newDatabase(awsClient aws_client.AwsClient) (*database, error) { if err != nil { return nil, err } - db, err := NewLockDatabase(dbConnection) + lockDb, err := NewLockDatabase(dbConnection) if err != nil { return nil, err } return &database{ - userConfigStorage: userStg, - lockDatabase: db, - sqlConnection: dbConnection, - AwsClient: awsClient, + userConfigStorage: userStg, + RemoteLockDatabase: lockDb, + sqlConnection: dbConnection, + AwsClient: awsClient, }, nil } diff --git a/db/interface.go b/db/interface.go index 124fb651..89bcfbf7 100644 --- a/db/interface.go +++ b/db/interface.go @@ -10,7 +10,7 @@ import ( const TfState = "terraform.tfstate" type LockDatabase interface { - LockConfig(ctx context.Context, userId string) (lock *ConfigLock, err error) + LockConfig(ctx context.Context, userId string, lockId string) (lock *ConfigLock, err error) UnlockConfig(ctx context.Context, lock *ConfigLock) error } @@ -23,8 +23,8 @@ type Database interface { LockDatabase GetUserId(ctx context.Context, apiKey string) (string, error) CreateUser(ctx context.Context, emailAddress string) (apiKey string, err error) - StoreUserConfig(ctx context.Context, config *configpb.Config, lock *ConfigLock) error - LoadUserConfig(ctx context.Context, userId string, lock *ConfigLock) (*configpb.Config, error) + StoreUserConfig(ctx context.Context, config *configpb.Config, configPrefix string, lock *ConfigLock) error + LoadUserConfig(ctx context.Context, userId string, configPrefix string, lock *ConfigLock) (*configpb.Config, error) Close() error } diff --git a/db/local_database.go b/db/local_database.go index 382087f3..f566bcaf 100644 --- a/db/local_database.go +++ b/db/local_database.go @@ -11,7 +11,7 @@ import ( type localDatabase struct { *userConfigStorage - lockDatabase *LocalLockDatabase + *LocalLockDatabase } func (d *localDatabase) Close() error { @@ -26,14 +26,6 @@ func (d *localDatabase) CreateUser(ctx context.Context, emailAddress string) (st return emailAddress, nil } -func (d *localDatabase) LockConfig(ctx context.Context, userId string) (lock *ConfigLock, err error) { - return d.lockDatabase.LockConfig(ctx, userId) -} - -func (d *localDatabase) UnlockConfig(ctx context.Context, lock *ConfigLock) error { - return d.lockDatabase.UnlockConfig(ctx, lock) -} - func (d *localDatabase) LoadTerraformState(_ context.Context, userId string) (string, error) { file, err := os.ReadFile(path.Join(filepath.Join(os.TempDir(), "multy", userId, "local"), TfState)) // empty state is fine and expected in dry runs @@ -50,6 +42,6 @@ func newLocalDatabase(awsClient aws_client.AwsClient) (*localDatabase, error) { } return &localDatabase{ userConfigStorage: userStg, - lockDatabase: newLocalLockDatabase(), + LocalLockDatabase: newLocalLockDatabase(), }, nil } diff --git a/db/local_lock_db.go b/db/local_lock_db.go index 83ae3ff7..21ae7b58 100644 --- a/db/local_lock_db.go +++ b/db/local_lock_db.go @@ -11,13 +11,14 @@ type LocalLockDatabase struct { lockCache sync.Map } -func (d *LocalLockDatabase) LockConfig(ctx context.Context, userId string) (lock *ConfigLock, err error) { +func (d *LocalLockDatabase) LockConfig(ctx context.Context, userId string, lockId string) (lock *ConfigLock, err error) { region := trace.StartRegion(ctx, "lock wait") defer region.End() - l, _ := d.lockCache.LoadOrStore(userId, &sync.Mutex{}) + l, _ := d.lockCache.LoadOrStore(lockId, &sync.Mutex{}) l.(*sync.Mutex).Lock() return &ConfigLock{ userId: userId, + lockId: lockId, expirationTimestamp: time.Now().Add(24 * time.Hour), active: true, }, nil @@ -27,7 +28,7 @@ func (d *LocalLockDatabase) UnlockConfig(_ context.Context, lock *ConfigLock) er if !lock.IsActive() { return nil } - l, _ := d.lockCache.Load(lock.userId) + l, _ := d.lockCache.Load(lock.lockId) l.(*sync.Mutex).Unlock() lock.active = false return nil diff --git a/db/lock_db.go b/db/lock_db.go index 1160dd41..53b2b521 100644 --- a/db/lock_db.go +++ b/db/lock_db.go @@ -34,8 +34,8 @@ type lockErr struct { error } -func (d *RemoteLockDatabase) LockConfig(ctx context.Context, userId string) (lock *ConfigLock, err error) { - localLock, err := d.localLockDatabase.LockConfig(ctx, userId) +func (d *RemoteLockDatabase) LockConfig(ctx context.Context, userId string, lockId string) (lock *ConfigLock, err error) { + localLock, err := d.localLockDatabase.LockConfig(ctx, userId, lockId) if err != nil { return nil, err } @@ -47,7 +47,7 @@ func (d *RemoteLockDatabase) LockConfig(ctx context.Context, userId string) (loc }() retryPeriod := lockRetryPeriod for { - configLock, err := d.lockConfig(ctx, userId) + configLock, err := d.lockConfig(ctx, userId, lockId) if err != nil { if !err.retryable { return nil, err.error @@ -75,7 +75,7 @@ func isRetryableSqlErr(err error) bool { return err != sql.ErrTxDone && err != sql.ErrConnDone } -func (d *RemoteLockDatabase) lockConfig(ctx context.Context, userId string) (*ConfigLock, *lockErr) { +func (d *RemoteLockDatabase) lockConfig(ctx context.Context, userId string, lockId string) (*ConfigLock, *lockErr) { log.Println("[DEBUG] locking") tx, err := d.sqlConnection.BeginTx(ctx, nil) if err != nil { @@ -95,13 +95,13 @@ func (d *RemoteLockDatabase) lockConfig(ctx context.Context, userId string) (*Co active: true, } err = d.sqlConnection. - QueryRowContext(ctx, "SELECT UserId, LockId, LockExpirationTimestamp FROM Locks WHERE UserId = ? AND LockId = ?;", userId, MainConfigLock). + QueryRowContext(ctx, "SELECT UserId, LockId, LockExpirationTimestamp FROM Locks WHERE UserId = ? AND LockId = ?;", userId, lockId). Scan(&row.userId, &row.lockId, &row.expirationTimestamp) now := time.Now().UTC() expirationTimestamp := now.Add(lockExpirationPeriod) if err == sql.ErrNoRows { _, err := d.sqlConnection. - ExecContext(ctx, "INSERT INTO Locks (UserId, LockId, LockExpirationTimestamp) VALUES (?, ?, ?);", userId, MainConfigLock, expirationTimestamp) + ExecContext(ctx, "INSERT INTO Locks (UserId, LockId, LockExpirationTimestamp) VALUES (?, ?, ?);", userId, lockId, expirationTimestamp) if err != nil { return nil, &lockErr{isRetryableSqlErr(err), err} } @@ -111,7 +111,7 @@ func (d *RemoteLockDatabase) lockConfig(ctx context.Context, userId string) (*Co } committed = true row.userId = userId - row.lockId = MainConfigLock + row.lockId = lockId row.expirationTimestamp = expirationTimestamp return &row, nil } else if err != nil { @@ -119,7 +119,7 @@ func (d *RemoteLockDatabase) lockConfig(ctx context.Context, userId string) (*Co } else if err == nil && now.After(row.expirationTimestamp) { log.Println("[WARNING] ConfigLock has expired, overwriting it") _, err := d.sqlConnection. - ExecContext(ctx, "UPDATE Locks SET LockExpirationTimestamp = ? WHERE UserId = ? AND LockId = ?;", expirationTimestamp, userId, MainConfigLock) + ExecContext(ctx, "UPDATE Locks SET LockExpirationTimestamp = ? WHERE UserId = ? AND LockId = ?;", expirationTimestamp, userId, lockId) if err != nil { return nil, &lockErr{isRetryableSqlErr(err), err} } diff --git a/db/user_config_storage.go b/db/user_config_storage.go index 539b4ee2..f1c0e41f 100644 --- a/db/user_config_storage.go +++ b/db/user_config_storage.go @@ -15,11 +15,11 @@ type userConfigStorage struct { AwsClient aws_client.AwsClient } -func (d *userConfigStorage) StoreUserConfig(ctx context.Context, config *configpb.Config, lock *ConfigLock) error { +func (d *userConfigStorage) StoreUserConfig(ctx context.Context, config *configpb.Config, configPrefix string, lock *ConfigLock) error { if !lock.IsActive() { return fmt.Errorf("unable to store user config because lock is invalid") } - log.Printf("[INFO] Storing user config from api_key %s\n", config.UserId) + log.Printf("[INFO] Storing user config for %s\n", configPrefix) region := trace.StartRegion(ctx, "config store") defer region.End() b, err := protojson.Marshal(config) @@ -27,25 +27,25 @@ func (d *userConfigStorage) StoreUserConfig(ctx context.Context, config *configp return err } - err = d.AwsClient.SaveFile(config.UserId, configFile, string(b)) + err = d.AwsClient.SaveFile(configPrefix, configFile, string(b)) if err != nil { return errors.InternalServerErrorWithMessage("error storing configuration", err) } return nil } -func (d *userConfigStorage) LoadUserConfig(ctx context.Context, userId string, lock *ConfigLock) (*configpb.Config, error) { +func (d *userConfigStorage) LoadUserConfig(ctx context.Context, userId string, configPrefix string, lock *ConfigLock) (*configpb.Config, error) { if lock != nil && !lock.IsActive() { return nil, fmt.Errorf("unable to load user config because lock is invalid") } - log.Printf("[INFO] Loading config from api_key %s\n", userId) + log.Printf("[INFO] Loading config from %s\n", configPrefix) region := trace.StartRegion(ctx, "config load") defer region.End() result := configpb.Config{ UserId: userId, } - configFileStr, err := d.AwsClient.ReadFile(userId, configFile) + configFileStr, err := d.AwsClient.ReadFile(configPrefix, configFile) if err != nil { return nil, errors.InternalServerErrorWithMessage("error reading configuration", err) } diff --git a/resources/common/resource_id.go b/resources/common/resource_id.go new file mode 100644 index 00000000..f060abdc --- /dev/null +++ b/resources/common/resource_id.go @@ -0,0 +1,23 @@ +package common + +import ( + "fmt" + "github.com/multycloud/multy/api/proto/commonpb" + "strings" +) + +func GetResourceId(prefix string, cloud commonpb.CloudProvider) string { + if cloud == commonpb.CloudProvider_UNKNOWN_PROVIDER { + return prefix + } + return fmt.Sprintf("%s_%s", prefix, strings.ToLower(cloud.String())) +} + +func ParseCloudFromResourceId(resourceId string) commonpb.CloudProvider { + split := strings.Split(resourceId, "_") + cloud := strings.ToUpper(split[len(split)-1]) + if cloudValue, ok := commonpb.CloudProvider_value[cloud]; ok { + return commonpb.CloudProvider(cloudValue) + } + return commonpb.CloudProvider_UNKNOWN_PROVIDER +} diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go index 9a404e90..41d6c10f 100644 --- a/resources/resource_metadata.go +++ b/resources/resource_metadata.go @@ -27,6 +27,7 @@ type ResourceExporter[ArgsT proto.Message] interface { Update(args ArgsT, others *Resources) error Import(resourceId string, args ArgsT, others *Resources) error Export(others *Resources) (ArgsT, bool, error) + ParseCloud(args ArgsT) commonpb.CloudProvider Resource } @@ -39,8 +40,14 @@ type ResourceMetadata[ArgsT proto.Message, R ResourceExporter[ArgsT], OutT proto ResourceType string } -func (m *ResourceMetadata[ArgsT, R, OutT]) Create(resourceId string, args proto.Message, resources *Resources) (Resource, error) { +func (m *ResourceMetadata[ArgsT, R, OutT]) ParseCloud(args proto.Message) commonpb.CloudProvider { r := reflect.New(reflect.TypeOf(*new(R)).Elem()).Interface().(R) + return r.ParseCloud(args.(ArgsT)) +} + +func (m *ResourceMetadata[ArgsT, R, OutT]) Create(resourceIdPrefix string, args proto.Message, resources *Resources) (Resource, error) { + r := reflect.New(reflect.TypeOf(*new(R)).Elem()).Interface().(R) + resourceId := common.GetResourceId(resourceIdPrefix, r.ParseCloud(args.(ArgsT))) err := r.Create(resourceId, args.(ArgsT), resources) return r, err } @@ -87,6 +94,7 @@ func (m *ResourceMetadata[ArgsT, R, OutT]) GetAbbreviatedName() string { type ResourceMetadataInterface interface { New() Resource + ParseCloud(proto.Message) commonpb.CloudProvider Create(string, proto.Message, *Resources) (Resource, error) Update(Resource, proto.Message, *Resources) error ReadFromState(Resource, *output.TfState) (proto.Message, error) @@ -120,7 +128,7 @@ func LoadConfig(c *configpb.Config, metadatas ResourceMetadatas) (*MultyConfig, res := NewResources() // we'll first add empty resources so that they can be used when calling Import() for _, r := range c.Resources { - conv, err := multyc.metadatas.getConverter(r.ResourceArgs.ResourceArgs.MessageName()) + conv, err := multyc.metadatas.GetConverter(r.ResourceArgs.ResourceArgs.MessageName()) if err != nil { return multyc, err } @@ -131,7 +139,7 @@ func LoadConfig(c *configpb.Config, metadatas ResourceMetadatas) (*MultyConfig, } for _, r := range c.Resources { - conv, err := multyc.metadatas.getConverter(r.ResourceArgs.ResourceArgs.MessageName()) + conv, err := multyc.metadatas.GetConverter(r.ResourceArgs.ResourceArgs.MessageName()) if err != nil { return multyc, err } @@ -169,15 +177,15 @@ func addMultyResource(r *configpb.Resource, res *Resources, metadata ResourceMet } func (c *MultyConfig) CreateResource(args proto.Message) (Resource, error) { - conv, err := c.metadatas.getConverter(proto.MessageName(args)) + conv, err := c.metadatas.GetConverter(proto.MessageName(args)) if err != nil { return nil, err } c.c.ResourceCounter += 1 - resourceId := fmt.Sprintf("multy_%s_u%s_r%d", conv.GetAbbreviatedName(), + resourceIdPrefix := fmt.Sprintf("multy_%s_u%s_r%d", conv.GetAbbreviatedName(), common.GenerateHash(c.c.UserId), c.c.ResourceCounter) - r, err := conv.Create(resourceId, args, c.Resources) + r, err := conv.Create(resourceIdPrefix, args, c.Resources) if err != nil { return nil, err } @@ -186,7 +194,7 @@ func (c *MultyConfig) CreateResource(args proto.Message) (Resource, error) { } func (c *MultyConfig) UpdateResource(resourceId string, args proto.Message) (Resource, error) { - conv, err := c.metadatas.getConverter(proto.MessageName(args)) + conv, err := c.metadatas.GetConverter(proto.MessageName(args)) if err != nil { return nil, err } @@ -288,7 +296,7 @@ func (c *MultyConfig) ExportConfig() (*configpb.Config, error) { return result, nil } -func (m ResourceMetadatas) getConverter(name protoreflect.FullName) (ResourceMetadataInterface, error) { +func (m ResourceMetadatas) GetConverter(name protoreflect.FullName) (ResourceMetadataInterface, error) { for messageType, conv := range m { if name == proto.MessageName(messageType) { return conv, nil diff --git a/resources/resource_with_id.go b/resources/resource_with_id.go index f0df5583..0a69ea88 100644 --- a/resources/resource_with_id.go +++ b/resources/resource_with_id.go @@ -51,8 +51,12 @@ func (r *ResourceWithId[T]) GetCloud() commonpb.CloudProvider { return r.Args.GetCommonParameters().CloudProvider } +func (r *ResourceWithId[T]) ParseCloud(args T) commonpb.CloudProvider { + return args.GetCommonParameters().CloudProvider +} + func (r *ResourceWithId[T]) GetMetadata(m ResourceMetadatas) (ResourceMetadataInterface, error) { - converter, err := m.getConverter(proto.MessageName(r.Args)) + converter, err := m.GetConverter(proto.MessageName(r.Args)) if err != nil { return nil, err } @@ -137,7 +141,7 @@ func (r *ChildResourceWithId[A, B]) GetCloudSpecificLocation() string { } func (r *ChildResourceWithId[A, B]) GetMetadata(m ResourceMetadatas) (ResourceMetadataInterface, error) { - converter, err := m.getConverter(proto.MessageName(r.Args)) + converter, err := m.GetConverter(proto.MessageName(r.Args)) if err != nil { return nil, err } diff --git a/resources/types/kubernetes_node_pool.go b/resources/types/kubernetes_node_pool.go index 1d579f39..16f14100 100644 --- a/resources/types/kubernetes_node_pool.go +++ b/resources/types/kubernetes_node_pool.go @@ -6,6 +6,7 @@ import ( "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -106,3 +107,7 @@ func (r *KubernetesNodePool) Validate(ctx resources.MultyContext) (errs []valida return errs } + +func (r *KubernetesNodePool) ParseCloud(args *resourcespb.KubernetesNodePoolArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.ClusterId) +} diff --git a/resources/types/network_interface_security_group_association.go b/resources/types/network_interface_security_group_association.go index 25f89434..e8b376f7 100644 --- a/resources/types/network_interface_security_group_association.go +++ b/resources/types/network_interface_security_group_association.go @@ -2,8 +2,10 @@ package types import ( "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -49,3 +51,7 @@ func NewNetworkInterfaceSecurityGroupAssociation(r *NetworkInterfaceSecurityGrou func (r *NetworkInterfaceSecurityGroupAssociation) Validate(ctx resources.MultyContext) (errs []validate.ValidationError) { return nil } + +func (r *NetworkInterfaceSecurityGroupAssociation) ParseCloud(args *resourcespb.NetworkInterfaceSecurityGroupAssociationArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.NetworkInterfaceId) +} diff --git a/resources/types/object_storage_object.go b/resources/types/object_storage_object.go index c08d3b80..3d3add8b 100644 --- a/resources/types/object_storage_object.go +++ b/resources/types/object_storage_object.go @@ -4,8 +4,10 @@ import ( "encoding/base64" "fmt" "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -54,3 +56,7 @@ func (r *ObjectStorageObject) Validate(ctx resources.MultyContext) (errs []valid } return errs } + +func (r *ObjectStorageObject) ParseCloud(args *resourcespb.ObjectStorageObjectArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.ObjectStorageId) +} diff --git a/resources/types/route_table.go b/resources/types/route_table.go index b97e486d..c3418bd9 100644 --- a/resources/types/route_table.go +++ b/resources/types/route_table.go @@ -2,6 +2,8 @@ package types import ( "fmt" + "github.com/multycloud/multy/api/proto/commonpb" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/api/errors" "github.com/multycloud/multy/api/proto/resourcespb" @@ -59,3 +61,7 @@ func (r *RouteTable) Validate(ctx resources.MultyContext) (errs []validate.Valid } return errs } + +func (r *RouteTable) ParseCloud(args *resourcespb.RouteTableArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.VirtualNetworkId) +} diff --git a/resources/types/route_table_association.go b/resources/types/route_table_association.go index f0fc689d..b60d1de7 100644 --- a/resources/types/route_table_association.go +++ b/resources/types/route_table_association.go @@ -3,8 +3,10 @@ package types import ( "fmt" "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -56,3 +58,7 @@ func (r *RouteTableAssociation) Validate(ctx resources.MultyContext) (errs []val } return errs } + +func (r *RouteTableAssociation) ParseCloud(args *resourcespb.RouteTableAssociationArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.RouteTableId) +} diff --git a/resources/types/subnet.go b/resources/types/subnet.go index fbec0e55..03ca18c0 100644 --- a/resources/types/subnet.go +++ b/resources/types/subnet.go @@ -3,8 +3,10 @@ package types import ( "github.com/apparentlymart/go-cidr/cidr" "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" "net" ) @@ -72,3 +74,7 @@ func (r *Subnet) Validate(ctx resources.MultyContext) (errs []validate.Validatio return errs } + +func (r *Subnet) ParseCloud(args *resourcespb.SubnetArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.VirtualNetworkId) +} diff --git a/resources/types/vault_access_policy.go b/resources/types/vault_access_policy.go index 3233bcc3..f792a990 100644 --- a/resources/types/vault_access_policy.go +++ b/resources/types/vault_access_policy.go @@ -3,8 +3,10 @@ package types import ( "fmt" "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -54,3 +56,7 @@ func (r *VaultAccessPolicy) Validate(ctx resources.MultyContext) (errs []validat } return errs } + +func (r *VaultAccessPolicy) ParseCloud(args *resourcespb.VaultAccessPolicyArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.VaultId) +} diff --git a/resources/types/vault_secret.go b/resources/types/vault_secret.go index 5f7e5575..79452d57 100644 --- a/resources/types/vault_secret.go +++ b/resources/types/vault_secret.go @@ -2,8 +2,10 @@ package types import ( "github.com/multycloud/multy/api/errors" + "github.com/multycloud/multy/api/proto/commonpb" "github.com/multycloud/multy/api/proto/resourcespb" "github.com/multycloud/multy/resources" + "github.com/multycloud/multy/resources/common" "github.com/multycloud/multy/validate" ) @@ -43,3 +45,7 @@ func NewVaultSecret(vs *VaultSecret, resourceId string, args *resourcespb.VaultS func (r *VaultSecret) Validate(ctx resources.MultyContext) (errs []validate.ValidationError) { return errs } + +func (r *VaultSecret) ParseCloud(args *resourcespb.VaultSecretArgs) commonpb.CloudProvider { + return common.ParseCloudFromResourceId(args.VaultId) +} diff --git a/test/deploy/deploy_test.go b/test/deploy/deploy_test.go index 03990cb8..69b59eff 100644 --- a/test/deploy/deploy_test.go +++ b/test/deploy/deploy_test.go @@ -95,7 +95,7 @@ func TestDeploy_rollbacksIfSomethingFails(t *testing.T) { mockTfCmd. On("GetState", mock.Anything, mock.Anything). Return(&output.TfState{}, nil) - _, err = sut.Deploy(ctx, config, nil, nil) + _, err = sut.Deploy(ctx, config, nil, nil, config.GetUserId()) assert.Error(t, err) mockTfCmd.AssertNumberOfCalls(t, "Apply", 2) @@ -145,7 +145,7 @@ func TestDeploy_callsTfApply(t *testing.T) { mockTfCmd. On("GetState", mock.Anything, mock.Anything). Return(&output.TfState{}, nil) - _, err = sut.Deploy(ctx, config, nil, nil) + _, err = sut.Deploy(ctx, config, nil, nil, config.GetUserId()) if err != nil { t.Fatalf("can't deploy, %s", err) } @@ -194,7 +194,7 @@ func TestDeploy_onlyAffectedResources(t *testing.T) { mockTfCmd. On("GetState", mock.Anything, mock.Anything). Return(&output.TfState{}, nil) - _, err = sut.Deploy(ctx, config, nil, r1) + _, err = sut.Deploy(ctx, config, nil, r1, config.GetUserId()) if err != nil { t.Fatalf("can't deploy, %s", err) } @@ -214,7 +214,7 @@ func TestDeploy_onlyAffectedResources(t *testing.T) { mockTfCmd. On("Apply", mock.Anything, mock.Anything, mock.Anything). Return(nil).Once() - _, err = sut.Deploy(ctx, config, nil, res2) + _, err = sut.Deploy(ctx, config, nil, res2, config.GetUserId()) if err != nil { t.Fatalf("can't deploy, %s", err) } @@ -291,7 +291,7 @@ func TestDeploy_rollbacksIfGetStateFails(t *testing.T) { mockTfCmd. On("GetState", mock.Anything, mock.Anything). Return(&output.TfState{}, nil) - rollbackFn, err := sut.Deploy(ctx, config, nil, nil) + rollbackFn, err := sut.Deploy(ctx, config, nil, nil, config.GetUserId()) if err != nil { t.Fatalf("error when deploying, %s", err) }