feat: [CDE-660]: Adding support for listing and (soft) deleting infra provider configs. (#3502)

* Fixing gitspace instance state enum.
* feat: [CDE-660]: Adding support for listing and (soft) deleting infra provider configs.
try-new-ui
Dhruv Dhruv 2025-03-04 06:35:19 +00:00 committed by Harness
parent 6d739f624a
commit 4d7c6addde
19 changed files with 413 additions and 35 deletions

View File

@ -0,0 +1,41 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 infraprovider
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types/enum"
)
func (c *Controller) DeleteConfig(
ctx context.Context,
session *auth.Session,
spaceRef string,
identifier string,
) error {
space, err := c.spaceFinder.FindByRef(ctx, spaceRef)
if err != nil {
return fmt.Errorf("failed to find space: %w", err)
}
err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, identifier, enum.PermissionInfraProviderDelete)
if err != nil {
return fmt.Errorf("failed to authorize: %w", err)
}
return c.infraproviderSvc.DeleteConfig(ctx, space, identifier)
}

View File

@ -0,0 +1,41 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 infraprovider
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
func (c *Controller) List(
ctx context.Context,
session *auth.Session,
spaceRef string,
) ([]*types.InfraProviderConfig, error) {
space, err := c.spaceFinder.FindByRef(ctx, spaceRef)
if err != nil {
return nil, fmt.Errorf("failed to find space: %w", err)
}
err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, "", enum.PermissionInfraProviderView)
if err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err)
}
return c.infraproviderSvc.List(ctx, space)
}

View File

@ -0,0 +1,49 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 infraprovider
import (
"net/http"
"github.com/harness/gitness/app/api/controller/infraprovider"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/app/paths"
)
func HandleDelete(infraProviderCtrl *infraprovider.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
infraProviderRefFromPath, err := request.GetInfraProviderRefFromPath(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
spaceRef, infraProviderIdentifier, err := paths.DisectLeaf(infraProviderRefFromPath)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
err = infraProviderCtrl.DeleteConfig(ctx, session, spaceRef, infraProviderIdentifier)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.DeleteSuccessful(w)
}
}

View File

@ -0,0 +1,44 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 space
import (
"net/http"
"github.com/harness/gitness/app/api/controller/infraprovider"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
)
func HandleListInfraProviderConfigs(infraProviderCtrl *infraprovider.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
infraProviderConfigs, err := infraProviderCtrl.List(ctx, session, spaceRef)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.JSON(w, http.StatusOK, infraProviderConfigs)
}
}

View File

@ -217,7 +217,7 @@ func setupRoutesV1WithAuth(r chi.Router,
usageSender usage.Sender,
) {
setupAccountWithAuth(r, userCtrl, config)
setupSpaces(r, appCtx, spaceCtrl, userGroupCtrl, webhookCtrl, checkCtrl)
setupSpaces(r, appCtx, infraProviderCtrl, spaceCtrl, userGroupCtrl, webhookCtrl, checkCtrl)
setupRepos(r, repoCtrl, repoSettingsCtrl, pipelineCtrl, executionCtrl, triggerCtrl,
logCtrl, pullreqCtrl, webhookCtrl, checkCtrl, uploadCtrl, usageSender)
setupConnectors(r, connectorCtrl)
@ -239,6 +239,7 @@ func setupRoutesV1WithAuth(r chi.Router,
func setupSpaces(
r chi.Router,
appCtx context.Context,
infraProviderCtrl *infraprovider.Controller,
spaceCtrl *space.Controller,
userGroupCtrl *usergroup.Controller,
webhookCtrl *webhook.Controller,
@ -271,6 +272,7 @@ func setupSpaces(
r.Get("/connectors", handlerspace.HandleListConnectors(spaceCtrl))
r.Get("/templates", handlerspace.HandleListTemplates(spaceCtrl))
r.Get("/gitspaces", handlerspace.HandleListGitspaces(spaceCtrl))
r.Get("/infraproviders", handlerspace.HandleListInfraProviderConfigs(infraProviderCtrl))
r.Post("/export", handlerspace.HandleExport(spaceCtrl))
r.Get("/export-progress", handlerspace.HandleExportProgress(spaceCtrl))
r.Post("/public-access", handlerspace.HandleUpdatePublicAccess(spaceCtrl))
@ -874,6 +876,7 @@ func setupInfraProviders(r chi.Router, infraProviderCtrl *infraprovider.Controll
r.Post("/", handlerinfraProvider.HandleCreateConfig(infraProviderCtrl))
r.Route(fmt.Sprintf("/{%s}", request.PathParamInfraProviderConfigIdentifier), func(r chi.Router) {
r.Get("/", handlerinfraProvider.HandleFind(infraProviderCtrl))
r.Delete("/", handlerinfraProvider.HandleDelete(infraProviderCtrl))
})
})
}

View File

@ -63,8 +63,10 @@ func (c *Service) fetchExistingConfigs(
ctx context.Context,
infraProviderConfig *types.InfraProviderConfig,
) ([]*types.InfraProviderConfig, error) {
existingConfigs, err := c.infraProviderConfigStore.FindByType(ctx, infraProviderConfig.SpaceID,
infraProviderConfig.Type)
existingConfigs, err := c.infraProviderConfigStore.List(ctx, &types.InfraProviderConfigFilter{
SpaceID: infraProviderConfig.SpaceID,
Type: infraProviderConfig.Type,
})
if err != nil {
return nil, fmt.Errorf("failed to find existing infraprovider config for type %s & space %d: %w",
infraProviderConfig.Type, infraProviderConfig.SpaceID, err)

View File

@ -0,0 +1,42 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 infraprovider
import (
"context"
"fmt"
"net/http"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/types"
)
func (c *Service) DeleteConfig(ctx context.Context, space *types.SpaceCore, identifier string) error {
err := c.tx.WithTx(ctx, func(ctx context.Context) error {
infraProviderConfig, err := c.Find(ctx, space, identifier)
if err != nil {
return fmt.Errorf("could not find infra provider config %s to delete: %w", identifier, err)
}
if len(infraProviderConfig.Resources) > 0 {
return usererror.Newf(http.StatusForbidden, "There are %d resources in this config. Deletion "+
"not allowed until all resources are deleted.", len(infraProviderConfig.Resources))
}
return c.infraProviderConfigStore.Delete(ctx, infraProviderConfig.ID)
})
if err != nil {
return err
}
return nil
}

View File

@ -51,8 +51,8 @@ func (c *Service) DeleteResource(
}
if len(activeGitspaces) > 0 {
return usererror.NewWithPayload(http.StatusForbidden, fmt.Sprintf("There are %d active configs for "+
"infra resource %s, expected 0", len(activeGitspaces), identifier))
return usererror.NewWithPayload(http.StatusForbidden, fmt.Sprintf("There are %d active gitspace "+
"configs for infra resource %s, expected 0", len(activeGitspaces), identifier))
}
return c.infraProviderResourceStore.Delete(ctx, infraProviderResource.ID)

View File

@ -31,32 +31,62 @@ func (c *Service) Find(
if err != nil {
return nil, fmt.Errorf("failed to find infraprovider config: %q %w", identifier, err)
}
err = c.populateDetails(ctx, space.Path, infraProviderConfig)
if err != nil {
return nil, err
}
return infraProviderConfig, nil
}
func (c *Service) populateDetails(
ctx context.Context,
spacePath string,
infraProviderConfig *types.InfraProviderConfig,
) error {
infraProviderConfig.SpacePath = spacePath
resources, err := c.getResources(ctx, spacePath, infraProviderConfig)
if err != nil {
return err
}
infraProviderConfig.Resources = resources
setupYAML, err := c.getSetupYAML(infraProviderConfig)
if err != nil {
return err
}
infraProviderConfig.SetupYAML = setupYAML
return nil
}
func (c *Service) getResources(
ctx context.Context,
spacePath string,
infraProviderConfig *types.InfraProviderConfig,
) ([]types.InfraProviderResource, error) {
resources, err := c.infraProviderResourceStore.List(ctx, infraProviderConfig.ID, types.ListQueryFilter{})
if err != nil {
return nil, fmt.Errorf("failed to find infraprovider resources for config: %q %w",
infraProviderConfig.Identifier, err)
}
infraProviderConfig.SpacePath = space.Path
var providerResources []types.InfraProviderResource
if len(resources) > 0 {
providerResources := make([]types.InfraProviderResource, len(resources))
providerResources = make([]types.InfraProviderResource, len(resources))
for i, resource := range resources {
if resource != nil {
providerResources[i] = *resource
providerResources[i].SpacePath = space.Path
providerResources[i].SpacePath = spacePath
}
}
slices.SortFunc(providerResources, types.CompareInfraProviderResource)
infraProviderConfig.Resources = providerResources
}
setupYAML, err := c.getSetupYAML(infraProviderConfig)
if err != nil {
return nil, err
}
infraProviderConfig.SetupYAML = setupYAML
return infraProviderConfig, nil
return providerResources, nil
}
func (c *Service) getSetupYAML(infraProviderConfig *types.InfraProviderConfig) (string, error) {

View File

@ -0,0 +1,43 @@
// Copyright 2023 Harness, Inc.
//
// Licensed 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 infraprovider
import (
"context"
"fmt"
"github.com/harness/gitness/types"
)
func (c *Service) List(
ctx context.Context,
space *types.SpaceCore,
) ([]*types.InfraProviderConfig, error) {
infraProviderConfigs, err := c.infraProviderConfigStore.List(ctx, &types.InfraProviderConfigFilter{
SpaceID: space.ID,
})
if err != nil {
return nil, fmt.Errorf("failed to list infraprovider configs: %w", err)
}
for _, infraProviderConfig := range infraProviderConfigs {
err = c.populateDetails(ctx, space.Path, infraProviderConfig)
if err != nil {
return nil, err
}
}
return infraProviderConfigs, nil
}

View File

@ -762,18 +762,17 @@ type (
// FindByIdentifier returns a infra provider config with a given UID in a space
FindByIdentifier(ctx context.Context, spaceID int64, identifier string) (*types.InfraProviderConfig, error)
// FindByType returns a infra provider config with a given type in a space
FindByType(
ctx context.Context,
spaceID int64,
infraProviderType enum.InfraProviderType,
) ([]*types.InfraProviderConfig, error)
// List returns all infra provider config matching the given filter
List(ctx context.Context, filter *types.InfraProviderConfigFilter) ([]*types.InfraProviderConfig, error)
// Create creates a new infra provider config in the datastore.
Create(ctx context.Context, infraProviderConfig *types.InfraProviderConfig) error
// Update tries to update the infra provider config in the datastore.
Update(ctx context.Context, infraProviderConfig *types.InfraProviderConfig) error
// Delete soft deletes the infra provider config given a ID from the datastore.
Delete(ctx context.Context, id int64) error
}
InfraProviderResourceStore interface {

View File

@ -17,6 +17,7 @@ package database
import (
"context"
"encoding/json"
"time"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database"
@ -24,6 +25,7 @@ import (
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
@ -37,7 +39,9 @@ const (
ipconf_space_id,
ipconf_created,
ipconf_updated,
ipconf_metadata
ipconf_metadata,
ipconf_is_deleted,
ipconf_deleted
`
infraProviderConfigSelectColumns = "ipconf_id," + infraProviderConfigInsertColumns
infraProviderConfigTable = `infra_provider_configs`
@ -52,6 +56,8 @@ type infraProviderConfig struct {
SpaceID int64 `db:"ipconf_space_id"`
Created int64 `db:"ipconf_created"`
Updated int64 `db:"ipconf_updated"`
IsDeleted bool `db:"ipconf_is_deleted"`
Deleted null.Int `db:"ipconf_deleted"`
}
var _ store.InfraProviderConfigStore = (*infraProviderConfigStore)(nil)
@ -93,7 +99,8 @@ func (i infraProviderConfigStore) Find(ctx context.Context, id int64) (*types.In
stmt := database.Builder.
Select(infraProviderConfigSelectColumns).
From(infraProviderConfigTable).
Where(infraProviderConfigIDColumn+" = $1", id) //nolint:goconst
Where("ipconf_is_deleted = false").
Where(infraProviderConfigIDColumn+" = ?", id) //nolint:goconst
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
@ -106,16 +113,23 @@ func (i infraProviderConfigStore) Find(ctx context.Context, id int64) (*types.In
return i.mapToInfraProviderConfig(dst)
}
func (i infraProviderConfigStore) FindByType(
func (i infraProviderConfigStore) List(
ctx context.Context,
spaceID int64,
infraProviderType enum.InfraProviderType,
filter *types.InfraProviderConfigFilter,
) ([]*types.InfraProviderConfig, error) {
stmt := database.Builder.
Select(infraProviderConfigSelectColumns).
From(infraProviderConfigTable).
Where("ipconf_type = $1", infraProviderType). //nolint:goconst
Where("ipconf_space_id = $2", spaceID)
Where("ipconf_is_deleted = false")
if filter != nil && filter.SpaceID > 0 {
stmt = stmt.Where("ipconf_space_id = ?", filter.SpaceID)
}
if filter != nil && filter.Type != "" {
stmt = stmt.Where("ipconf_type = ?", filter.Type)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
@ -124,8 +138,7 @@ func (i infraProviderConfigStore) FindByType(
db := dbtx.GetAccessor(ctx, i.db)
dst := new([]*infraProviderConfig)
if err := db.SelectContext(ctx, dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list infraprovider configs of type %s for"+
" space %d", infraProviderType, spaceID)
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list infraprovider configs")
}
return i.mapToInfraProviderConfigs(*dst)
}
@ -138,8 +151,9 @@ func (i infraProviderConfigStore) FindByIdentifier(
stmt := database.Builder.
Select(infraProviderConfigSelectColumns).
From(infraProviderConfigTable).
Where("ipconf_uid = $1", identifier). //nolint:goconst
Where("ipconf_space_id = $2", spaceID)
Where("ipconf_is_deleted = false").
Where("ipconf_uid = ?", identifier). //nolint:goconst
Where("ipconf_space_id = ?", spaceID)
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
@ -168,6 +182,8 @@ func (i infraProviderConfigStore) Create(ctx context.Context, infraProviderConfi
dbinfraProviderConfig.Created,
dbinfraProviderConfig.Updated,
dbinfraProviderConfig.Metadata,
dbinfraProviderConfig.IsDeleted,
dbinfraProviderConfig.Deleted,
).
Suffix(ReturningClause + infraProviderConfigIDColumn)
sql, args, err := stmt.ToSql()
@ -182,6 +198,25 @@ func (i infraProviderConfigStore) Create(ctx context.Context, infraProviderConfi
return nil
}
func (i infraProviderConfigStore) Delete(ctx context.Context, id int64) error {
now := time.Now().UnixMilli()
stmt := database.Builder.
Update(infraProviderConfigTable).
Set("ipconf_updated", now).
Set("ipconf_deleted", now).
Set("ipconf_is_deleted", true).
Where(infraProviderConfigIDColumn+" = ?", id)
sql, args, err := stmt.ToSql()
if err != nil {
return errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, i.db)
if _, err := db.ExecContext(ctx, sql, args...); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to update infraprovider config %d", id)
}
return nil
}
func (i infraProviderConfigStore) mapToInfraProviderConfig(
in *infraProviderConfig,
) (*types.InfraProviderConfig, error) {
@ -201,6 +236,8 @@ func (i infraProviderConfigStore) mapToInfraProviderConfig(
SpaceID: in.SpaceID,
Created: in.Created,
Updated: in.Updated,
IsDeleted: in.IsDeleted,
Deleted: in.Deleted.Ptr(),
}
return infraProviderConfigEntity, nil
}
@ -238,6 +275,8 @@ func (i infraProviderConfigStore) mapToInternalInfraProviderConfig(
Created: in.Created,
Updated: in.Updated,
Metadata: jsonBytes,
IsDeleted: in.IsDeleted,
Deleted: null.IntFromPtr(in.Deleted),
}
return infraProviderConfigEntity, nil
}

View File

@ -0,0 +1,9 @@
DROP INDEX infra_provider_configs_uid_space_id_created;
ALTER TABLE infra_provider_configs
DROP COLUMN ipconf_is_deleted;
ALTER TABLE infra_provider_configs
DROP COLUMN ipconf_deleted;
CREATE UNIQUE INDEX infra_provider_configs_uid_space_id ON infra_provider_configs (ipconf_uid, ipconf_space_id);

View File

@ -0,0 +1,8 @@
ALTER TABLE infra_provider_configs
ADD COLUMN ipconf_is_deleted BOOL NOT NULL DEFAULT false;
ALTER TABLE infra_provider_configs
ADD COLUMN ipconf_deleted BIGINT;
DROP INDEX infra_provider_configs_uid_space_id;
CREATE UNIQUE INDEX infra_provider_configs_uid_space_id_created ON infra_provider_configs
(ipconf_uid, ipconf_space_id, ipconf_created);

View File

@ -0,0 +1,9 @@
DROP INDEX infra_provider_configs_uid_space_id_created;
ALTER TABLE infra_provider_configs
DROP COLUMN ipconf_is_deleted;
ALTER TABLE infra_provider_configs
DROP COLUMN ipconf_deleted;
CREATE UNIQUE INDEX infra_provider_configs_uid_space_id ON infra_provider_configs (ipconf_uid, ipconf_space_id);

View File

@ -0,0 +1,8 @@
ALTER TABLE infra_provider_configs
ADD COLUMN ipconf_is_deleted BOOL NOT NULL DEFAULT false;
ALTER TABLE infra_provider_configs
ADD COLUMN ipconf_deleted BIGINT;
DROP INDEX infra_provider_configs_uid_space_id;
CREATE UNIQUE INDEX infra_provider_configs_uid_space_id_created ON infra_provider_configs
(ipconf_uid, ipconf_space_id, ipconf_created);

View File

@ -28,6 +28,8 @@ var gitspaceInstanceStateTypes = []GitspaceInstanceStateType{
GitspaceInstanceStateDeleted,
GitspaceInstanceStateStarting,
GitspaceInstanceStateStopping,
GitSpaceInstanceStateCleaning,
GitspaceInstanceStateCleaned,
}
const (

View File

@ -21,6 +21,8 @@ import (
"github.com/harness/gitness/types/enum"
)
const emptyGitspaceInstanceState = ""
type GitspaceConfig struct {
ID int64 `json:"-"`
Identifier string `json:"identifier"`
@ -117,7 +119,7 @@ func (g *GitspaceInstance) GetGitspaceState() (enum.GitspaceStateType, error) {
return enum.GitspaceStateRunning, nil
case enum.GitspaceInstanceStateDeleted:
return enum.GitspaceStateStopped, nil
case enum.GitspaceInstanceStateUninitialized:
case emptyGitspaceInstanceState, enum.GitspaceInstanceStateUninitialized:
return enum.GitspaceStateUninitialized, nil
case enum.GitspaceInstanceStateError,
enum.GitspaceInstanceStateUnknown:

View File

@ -36,6 +36,8 @@ type InfraProviderConfig struct {
Created int64 `json:"created"`
Updated int64 `json:"updated"`
SetupYAML string `json:"setup_yaml,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
Deleted *int64 `json:"deleted,omitempty"`
}
type InfraProviderResource struct {
@ -158,3 +160,8 @@ type InfraProviderTemplate struct {
Created int64 `json:"created"`
Updated int64 `json:"updated"`
}
type InfraProviderConfigFilter struct {
SpaceID int64
Type enum.InfraProviderType
}