diff --git a/app/gitspace/infrastructure/provisioner.go b/app/gitspace/infrastructure/provisioner.go index 6eea22f9a6..cafbfe28a5 100644 --- a/app/gitspace/infrastructure/provisioner.go +++ b/app/gitspace/infrastructure/provisioner.go @@ -28,35 +28,28 @@ type InfraProvisioner interface { // stores the details in the db depending on the provisioning type. Provision( ctx context.Context, - spaceID int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - gitspaceInstanceID int64, - gitspaceInstanceIdentifier string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) + // Stop unprovisions those resources which can be stopped without losing the gitspace data. Stop( ctx context.Context, - spaceID int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - gitspaceInstanceID int64, - gitspaceInstanceIdentifier string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) + // Unprovision unprovisions all the resources created for the gitspace. Unprovision( ctx context.Context, - spaceID int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - gitspaceInstanceID int64, - gitspaceInstanceIdentifier string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) + // Find finds the provisioned infra resources for the gitspace instance. Find( ctx context.Context, - spaceID int64, infraProviderResource *types.InfraProviderResource, - gitspaceInstanceID int64, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) } diff --git a/app/gitspace/infrastructure/provisioner_impl.go b/app/gitspace/infrastructure/provisioner_impl.go index 2e8476a60c..282390c763 100644 --- a/app/gitspace/infrastructure/provisioner_impl.go +++ b/app/gitspace/infrastructure/provisioner_impl.go @@ -46,11 +46,8 @@ func NewInfraProvisionerService( func (i infraProvisioner) Provision( ctx context.Context, - _ int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - _ int64, - _ string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) { infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource) if err != nil { @@ -83,11 +80,11 @@ func (i infraProvisioner) Provision( return nil, fmt.Errorf("invalid provisioning params %v: %w", infraProviderResource.Metadata, err) } - provisionedInfra, err := infraProvider.Provision(ctx, gitspaceConfigIdentifier, allParams) + provisionedInfra, err := infraProvider.Provision(ctx, gitspaceConfig.Identifier, allParams) if err != nil { return nil, fmt.Errorf( "unable to provision infrastructure for gitspaceConfigIdentifier %v: %w", - gitspaceConfigIdentifier, + gitspaceConfig.Identifier, err, ) } @@ -101,11 +98,8 @@ func (i infraProvisioner) Provision( func (i infraProvisioner) Stop( ctx context.Context, - _ int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - _ int64, - _ string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) { infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource) if err != nil { @@ -144,7 +138,7 @@ func (i infraProvisioner) Stop( // TODO: Fetch and check existing infraProvisioned record } else { provisionedInfra = infraprovider.Infrastructure{ - ResourceKey: gitspaceConfigIdentifier, + ResourceKey: gitspaceConfig.Identifier, ProviderType: infraProviderEntity.Type, Parameters: allParams, } @@ -164,11 +158,8 @@ func (i infraProvisioner) Stop( func (i infraProvisioner) Unprovision( ctx context.Context, - _ int64, infraProviderResource *types.InfraProviderResource, - gitspaceConfigIdentifier string, - _ int64, - _ string, + gitspaceConfig *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) { infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource) if err != nil { @@ -207,7 +198,7 @@ func (i infraProvisioner) Unprovision( // TODO: Fetch and check existing infraProvisioned record } else { provisionedInfra = infraprovider.Infrastructure{ - ResourceKey: gitspaceConfigIdentifier, + ResourceKey: gitspaceConfig.Identifier, ProviderType: infraProviderEntity.Type, Parameters: allParams, } @@ -227,9 +218,8 @@ func (i infraProvisioner) Unprovision( func (i infraProvisioner) Find( ctx context.Context, - _ int64, infraProviderResource *types.InfraProviderResource, - _ int64, + _ *types.GitspaceConfig, ) (*infraprovider.Infrastructure, error) { infraProviderEntity, err := i.getConfigFromResource(ctx, infraProviderResource) if err != nil { diff --git a/app/gitspace/orchestrator/container/container_orchestrator.go b/app/gitspace/orchestrator/container/container_orchestrator.go index 9217282a78..e11267a866 100644 --- a/app/gitspace/orchestrator/container/container_orchestrator.go +++ b/app/gitspace/orchestrator/container/container_orchestrator.go @@ -19,7 +19,6 @@ import ( "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/types" - "github.com/harness/gitness/types/enum" ) type Orchestrator interface { @@ -30,7 +29,7 @@ type Orchestrator interface { gitspaceConfig *types.GitspaceConfig, devcontainerConfig *types.DevcontainerConfig, infra *infraprovider.Infrastructure, - ) (map[enum.IDEType]string, error) + ) (*StartResponse, error) // StopGitspace stops and removes the gitspace container. StopGitspace(ctx context.Context, gitspaceConfig *types.GitspaceConfig, infra *infraprovider.Infrastructure) error diff --git a/app/gitspace/orchestrator/container/embedded_docker.go b/app/gitspace/orchestrator/container/embedded_docker.go index 7e86309bd7..f2c79464d4 100644 --- a/app/gitspace/orchestrator/container/embedded_docker.go +++ b/app/gitspace/orchestrator/container/embedded_docker.go @@ -84,7 +84,7 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace( gitspaceConfig *types.GitspaceConfig, devcontainerConfig *types.DevcontainerConfig, infra *infraprovider.Infrastructure, -) (map[enum.IDEType]string, error) { +) (*StartResponse, error) { containerName := getGitspaceContainerName(gitspaceConfig) log := log.Ctx(ctx).With().Str(loggingKey, containerName).Logger() @@ -108,7 +108,7 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace( } var usedPorts map[enum.IDEType]string - + var containerID string switch state { case containerStateRunning: log.Debug().Msg("gitspace is already running") @@ -118,11 +118,13 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace( return nil, startErr } - ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService) + id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, ideService) if startErr != nil { return nil, startErr } + usedPorts = ports + containerID = id case containerStateRemoved: log.Debug().Msg("gitspace is not running, starting it...") @@ -143,10 +145,12 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace( if startErr != nil { return nil, fmt.Errorf("failed to start gitspace %s: %w", containerName, startErr) } - ports, startErr := e.getUsedPorts(ctx, containerName, dockerClient, ideService) + id, ports, startErr := e.getContainerInfo(ctx, containerName, dockerClient, ideService) if startErr != nil { return nil, startErr } + + containerID = id usedPorts = ports // TODO: Add gitspace status reporting. @@ -156,7 +160,11 @@ func (e *EmbeddedDockerOrchestrator) StartGitspace( return nil, fmt.Errorf("gitspace %s is in a bad state: %s", containerName, state) } - return usedPorts, nil + return &StartResponse{ + ContainerID: containerID, + ContainerName: containerName, + PortsUsed: usedPorts, + }, nil } func (e *EmbeddedDockerOrchestrator) startGitspace( @@ -211,15 +219,15 @@ func (e *EmbeddedDockerOrchestrator) startGitspace( return nil } -func (e *EmbeddedDockerOrchestrator) getUsedPorts( +func (e *EmbeddedDockerOrchestrator) getContainerInfo( ctx context.Context, containerName string, dockerClient *client.Client, ideService IDE, -) (map[enum.IDEType]string, error) { +) (string, map[enum.IDEType]string, error) { inspectResp, err := dockerClient.ContainerInspect(ctx, containerName) if err != nil { - return nil, fmt.Errorf("could not inspect container %s: %w", containerName, err) + return "", nil, fmt.Errorf("could not inspect container %s: %w", containerName, err) } usedPorts := map[enum.IDEType]string{} @@ -232,7 +240,7 @@ func (e *EmbeddedDockerOrchestrator) getUsedPorts( } } - return usedPorts, nil + return inspectResp.ID, usedPorts, nil } func (e *EmbeddedDockerOrchestrator) getIDEService(gitspaceConfig *types.GitspaceConfig) (IDE, error) { diff --git a/app/gitspace/orchestrator/container/types.go b/app/gitspace/orchestrator/container/types.go new file mode 100644 index 0000000000..f13d746b31 --- /dev/null +++ b/app/gitspace/orchestrator/container/types.go @@ -0,0 +1,23 @@ +// 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 container + +import "github.com/harness/gitness/types/enum" + +type StartResponse struct { + ContainerID string + ContainerName string + PortsUsed map[enum.IDEType]string +} diff --git a/app/gitspace/orchestrator/orchestrator.go b/app/gitspace/orchestrator/orchestrator.go new file mode 100644 index 0000000000..3acce98020 --- /dev/null +++ b/app/gitspace/orchestrator/orchestrator.go @@ -0,0 +1,45 @@ +// 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 orchestrator + +import ( + "context" + + "github.com/harness/gitness/types" +) + +type Orchestrator interface { + // StartGitspace is responsible for all the operations necessary to create the Gitspace container. It fetches the + // devcontainer.json from the code repo, provisions infra using the infra provisioner and setting up the Gitspace + // through the container orchestrator. + StartGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, + ) (*types.GitspaceInstance, error) + + // StopGitspace is responsible for stopping a running Gitspace. It stops the Gitspace container and unprovisions + // all the infra resources which are not required to restart the Gitspace. + StopGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, + ) (*types.GitspaceInstance, error) + + // DeleteGitspace is responsible for deleting a Gitspace. It stops the Gitspace container and unprovisions + // all the infra resources. + DeleteGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, + ) (*types.GitspaceInstance, error) +} diff --git a/app/gitspace/orchestrator/orchestrator_impl.go b/app/gitspace/orchestrator/orchestrator_impl.go new file mode 100644 index 0000000000..33c3645b86 --- /dev/null +++ b/app/gitspace/orchestrator/orchestrator_impl.go @@ -0,0 +1,197 @@ +// 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 orchestrator + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/harness/gitness/app/gitspace/infrastructure" + "github.com/harness/gitness/app/gitspace/orchestrator/container" + "github.com/harness/gitness/app/gitspace/scm" + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/guregu/null" + "github.com/rs/zerolog/log" +) + +type orchestrator struct { + scm scm.SCM + infraProviderResourceStore store.InfraProviderResourceStore + infraProvisioner infrastructure.InfraProvisioner + containerOrchestrator container.Orchestrator +} + +var _ Orchestrator = (*orchestrator)(nil) + +func NewOrchestrator( + scm scm.SCM, + infraProviderResourceStore store.InfraProviderResourceStore, + infraProvisioner infrastructure.InfraProvisioner, + containerOrchestrator container.Orchestrator, +) Orchestrator { + return orchestrator{ + scm: scm, + infraProviderResourceStore: infraProviderResourceStore, + infraProvisioner: infraProvisioner, + containerOrchestrator: containerOrchestrator, + } +} + +func (o orchestrator) StartGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, +) (*types.GitspaceInstance, error) { + devcontainerConfig, err := o.scm.DevcontainerConfig(ctx, gitspaceConfig) + if err != nil { + log.Warn().Err(err).Msg("devcontainerConfig fetch failed.") + } + + if devcontainerConfig == nil { + log.Warn().Err(err).Msg("devcontainerConfig is nil, using empty config") + devcontainerConfig = &types.DevcontainerConfig{} + } + + infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) + if err != nil { + return nil, fmt.Errorf("cannot get the infraProviderResource for ID %d: %w", + gitspaceConfig.InfraProviderResourceID, err) + } + + infra, err := o.infraProvisioner.Provision(ctx, infraProviderResource, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf("cannot provision infrastructure for ID %d: %w", + gitspaceConfig.InfraProviderResourceID, err) + } + + gitspaceInstance := gitspaceConfig.GitspaceInstance + + err = o.containerOrchestrator.Status(ctx, infra) + gitspaceInstance.State = enum.GitspaceInstanceStateError + if err != nil { + return gitspaceInstance, fmt.Errorf("couldn't call the agent health API: %w", err) + } + + startResponse, err := o.containerOrchestrator.StartGitspace(ctx, gitspaceConfig, devcontainerConfig, infra) + if err != nil { + return gitspaceInstance, fmt.Errorf("couldn't call the agent start API: %w", err) + } + + repoName, err := o.scm.RepositoryName(ctx, gitspaceConfig) + if err != nil { + log.Warn().Err(err).Msg("failed to fetch repository name.") + } + + port := startResponse.PortsUsed[gitspaceConfig.IDE] + + var ideURL url.URL + + if gitspaceConfig.IDE == enum.IDETypeVSCodeWeb { + ideURL = url.URL{ + Scheme: "http", + Host: infra.Host + ":" + port, + RawQuery: "folder=/gitspace/" + repoName, + } + } else if gitspaceConfig.IDE == enum.IDETypeVSCode { + // TODO: the following user ID is hard coded and should be changed. + ideURL = url.URL{ + Scheme: "vscode-remote", + Host: "", // Empty since we include the host and port in the path + Path: fmt.Sprintf("ssh-remote+%s@%s:%s/gitspace/%s", "harness", infra.Host, port, repoName), + } + } + gitspaceInstance.URL = null.NewString(ideURL.String(), true) + + gitspaceInstance.LastUsed = time.Now().UnixMilli() + gitspaceInstance.State = enum.GitspaceInstanceStateRunning + + return gitspaceInstance, nil +} + +func (o orchestrator) StopGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, +) (*types.GitspaceInstance, error) { + infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) + if err != nil { + return nil, fmt.Errorf( + "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) + } + + infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf("cannot find the provisioned infra: %w", err) + } + + err = o.containerOrchestrator.StopGitspace(ctx, gitspaceConfig, infra) + if err != nil { + return nil, fmt.Errorf("error stopping the Gitspace container: %w", err) + } + + _, err = o.infraProvisioner.Stop(ctx, infraProviderResource, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf( + "cannot stop provisioned infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) + } + + gitspaceInstance := gitspaceConfig.GitspaceInstance + gitspaceInstance.State = enum.GitspaceInstanceStateDeleted + gitspaceInstance.URL = null.NewString("", false) + + return gitspaceInstance, err +} + +func (o orchestrator) DeleteGitspace( + ctx context.Context, + gitspaceConfig *types.GitspaceConfig, +) (*types.GitspaceInstance, error) { + var updatedGitspaceInstance *types.GitspaceInstance + + gitspaceInstance := gitspaceConfig.GitspaceInstance + + if gitspaceInstance.State == enum.GitspaceInstanceStateRunning || + gitspaceInstance.State == enum.GitspaceInstanceStateUnknown { + infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) + if err != nil { + return nil, fmt.Errorf( + "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) + } + + infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf("cannot find the provisioned infra: %w", err) + } + + err = o.containerOrchestrator.StopGitspace(ctx, gitspaceConfig, infra) + if err != nil { + return nil, fmt.Errorf("error stopping the Gitspace container: %w", err) + } + + _, err = o.infraProvisioner.Unprovision(ctx, infraProviderResource, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf( + "cannot stop provisioned infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) + } + + gitspaceInstance.State = enum.GitspaceInstanceStateDeleted + updatedGitspaceInstance = gitspaceInstance + } + + return updatedGitspaceInstance, nil +} diff --git a/app/gitspace/orchestrator/wire.go b/app/gitspace/orchestrator/wire.go new file mode 100644 index 0000000000..1696c0e6d0 --- /dev/null +++ b/app/gitspace/orchestrator/wire.go @@ -0,0 +1,38 @@ +// 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 orchestrator + +import ( + "github.com/harness/gitness/app/gitspace/infrastructure" + "github.com/harness/gitness/app/gitspace/orchestrator/container" + "github.com/harness/gitness/app/gitspace/scm" + "github.com/harness/gitness/app/store" + + "github.com/google/wire" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideOrchestrator, +) + +func ProvideOrchestrator( + scm scm.SCM, + infraProviderResourceStore store.InfraProviderResourceStore, + infraProvisioner infrastructure.InfraProvisioner, + containerOrchestrator container.Orchestrator, +) Orchestrator { + return NewOrchestrator(scm, infraProviderResourceStore, infraProvisioner, containerOrchestrator) +} diff --git a/app/gitspace/scm/scm.go b/app/gitspace/scm/scm.go index bcc916ab2b..0251d9e19a 100644 --- a/app/gitspace/scm/scm.go +++ b/app/gitspace/scm/scm.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "net/url" "os" "regexp" "strings" @@ -36,6 +37,8 @@ var _ SCM = (*scm)(nil) type SCM interface { // DevcontainerConfig fetches devcontainer config file from the given repo and branch. DevcontainerConfig(ctx context.Context, gitspaceConfig *types.GitspaceConfig) (*types.DevcontainerConfig, error) + // RepositoryName finds the repository name for the code repo URL from its provider. + RepositoryName(ctx context.Context, gitspaceConfig *types.GitspaceConfig) (string, error) } type scm struct{} @@ -128,6 +131,22 @@ func (s scm) DevcontainerConfig( return &config, nil } +// TODO: Make RepositoryName compatible with all SCM providers + +func (s scm) RepositoryName(_ context.Context, gitspaceConfig *types.GitspaceConfig) (string, error) { + parsedURL, err := url.Parse(gitspaceConfig.CodeRepoURL) + if err != nil { + return "", fmt.Errorf("failed to parse url: %w", err) + } + pathSegments := strings.Split(parsedURL.Path, "/") + + if len(pathSegments) < 3 || pathSegments[1] == "" || pathSegments[2] == "" { + return "", fmt.Errorf("invalid repository name URL: %s", parsedURL.String()) + } + repoName := pathSegments[2] + return strings.ReplaceAll(repoName, ".git", ""), nil +} + func removeComments(input []byte) []byte { blockCommentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`) input = blockCommentRegex.ReplaceAll(input, nil) diff --git a/types/gitspace.go b/types/gitspace.go index 0c115bdf34..af786dabf0 100644 --- a/types/gitspace.go +++ b/types/gitspace.go @@ -45,23 +45,23 @@ type GitspaceConfig struct { } type GitspaceInstance struct { - ID int64 `json:"-"` - GitSpaceConfigID int64 `json:"-"` - Identifier string `json:"identifier"` - URL null.String `json:"url,omitempty"` - State enum.GitspaceStateType `json:"state"` - UserID string `json:"-"` - ResourceUsage null.String `json:"resource_usage"` - LastUsed int64 `json:"last_used,omitempty"` - TotalTimeUsed int64 `json:"total_time_used"` - TrackedChanges string `json:"tracked_changes"` - AccessKey null.String `json:"access_key,omitempty"` - AccessType enum.GitspaceAccessType `json:"access_type"` - MachineUser null.String `json:"machine_user,omitempty"` - SpacePath string `json:"space_path"` - SpaceID int64 `json:"-"` - Created int64 `json:"created"` - Updated int64 `json:"updated"` + ID int64 `json:"-"` + GitSpaceConfigID int64 `json:"-"` + Identifier string `json:"identifier"` + URL null.String `json:"url,omitempty"` + State enum.GitspaceInstanceStateType `json:"state"` + UserID string `json:"-"` + ResourceUsage null.String `json:"resource_usage"` + LastUsed int64 `json:"last_used,omitempty"` + TotalTimeUsed int64 `json:"total_time_used"` + TrackedChanges string `json:"tracked_changes"` + AccessKey null.String `json:"access_key,omitempty"` + AccessType enum.GitspaceAccessType `json:"access_type"` + MachineUser null.String `json:"machine_user,omitempty"` + SpacePath string `json:"space_path"` + SpaceID int64 `json:"-"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` } type GitspaceFilter struct {