feat: [CDE-137]: check valid code repository for CDE use. (#2197)

* feat: [CDE-137]: check valid code repository for CDE use.
* feat: [CDE-137]: check valid code repository for CDE use.
* feat: [CDE-137]: check valid code repository for CDE use.
unified-ui
Ansuman Satapathy 2024-07-11 05:47:29 +00:00 committed by Harness
parent 76682ad400
commit 18226c8914
12 changed files with 227 additions and 22 deletions

View File

@ -29,10 +29,12 @@ import (
"github.com/harness/gitness/types/enum"
gonanoid "github.com/matoous/go-nanoid"
"github.com/rs/zerolog/log"
)
const defaultAccessKey = "Harness@123"
const defaultMachineUser = "harness"
const gitspaceTimedOutInMintues = 5
type ActionInput struct {
Action enum.GitspaceActionType `json:"action"`
@ -115,7 +117,10 @@ func (c *Controller) startGitspaceAction(
config.State, _ = enum.GetGitspaceStateFromInstance(newGitspaceInstance.State)
ctx2 := context.WithoutCancel(ctx)
go func() {
_, _ = c.startAsyncOperation(ctx2, config)
err := c.startAsyncOperation(ctx2, config)
if err != nil {
log.Err(err).Msg("start operation failed")
}
}()
return config, nil
}
@ -123,17 +128,15 @@ func (c *Controller) startGitspaceAction(
func (c *Controller) startAsyncOperation(
ctx context.Context,
config *types.GitspaceConfig,
) (*types.GitspaceConfig, error) {
) error {
updatedGitspace, orchestrateErr := c.orchestrator.StartGitspace(ctx, config)
if err := c.gitspaceInstanceStore.Update(ctx, updatedGitspace); err != nil {
return nil, fmt.Errorf("failed to update gitspace %w %w", err, orchestrateErr)
return fmt.Errorf("failed to update gitspace %w %w", err, orchestrateErr)
}
if orchestrateErr != nil {
return nil, fmt.Errorf("failed to find start gitspace : %s %w", config.Identifier, orchestrateErr)
return fmt.Errorf("failed to find start gitspace : %s %w", config.Identifier, orchestrateErr)
}
config.GitspaceInstance = updatedGitspace
config.State, _ = enum.GetGitspaceStateFromInstance(updatedGitspace.State)
return config, nil
return nil
}
func (c *Controller) createGitspaceInstance(config *types.GitspaceConfig) (*types.GitspaceInstance, error) {
@ -171,9 +174,8 @@ func (c *Controller) gitspaceBusyOperation(
if config.GitspaceInstance == nil {
return config, nil
}
const timedOutInSeconds = 5
if config.GitspaceInstance.State.IsBusyStatus() &&
time.Since(time.UnixMilli(config.GitspaceInstance.Updated)).Milliseconds() <= (timedOutInSeconds*60*1000) {
time.Since(time.UnixMilli(config.GitspaceInstance.Updated)).Milliseconds() <= (gitspaceTimedOutInMintues*60*1000) {
return nil, fmt.Errorf("gitspace start/stop is already pending for : %q", config.Identifier)
} else if config.GitspaceInstance.State.IsBusyStatus() {
config.GitspaceInstance.State = enum.GitspaceInstanceStateError
@ -208,7 +210,10 @@ func (c *Controller) stopGitspaceAction(
config.State, _ = enum.GetGitspaceStateFromInstance(savedGitspace.State)
ctx2 := context.WithoutCancel(ctx)
go func() {
_, _ = c.stopAsyncOperation(ctx2, config)
err := c.stopAsyncOperation(ctx2, config)
if err != nil {
log.Err(err).Msg("stop operation failed")
}
}()
return config, err
}
@ -216,25 +221,23 @@ func (c *Controller) stopGitspaceAction(
func (c *Controller) stopAsyncOperation(
ctx context.Context,
config *types.GitspaceConfig,
) (*types.GitspaceConfig, error) {
) error {
savedGitspace := config.GitspaceInstance
updatedGitspace, orchestrateErr := c.orchestrator.StopGitspace(ctx, config)
if updatedGitspace != nil {
if err := c.gitspaceInstanceStore.Update(ctx, updatedGitspace); err != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"unable to update the gitspace with config id %s %w %w",
savedGitspace.Identifier,
err,
orchestrateErr)
}
if orchestrateErr != nil {
return nil, fmt.Errorf(
return fmt.Errorf(
"failed to stop gitspace instance with ID %s %w", savedGitspace.Identifier, orchestrateErr)
}
}
config.GitspaceInstance = updatedGitspace
config.State, _ = enum.GetGitspaceStateFromInstance(updatedGitspace.State)
return config, nil
return nil
}
func (c *Controller) sanitizeActionInput(in *ActionInput) error {

View File

@ -19,6 +19,7 @@ import (
gitspaceevents "github.com/harness/gitness/app/events/gitspace"
"github.com/harness/gitness/app/gitspace/logutil"
"github.com/harness/gitness/app/gitspace/orchestrator"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database/dbtx"
)
@ -34,6 +35,7 @@ type Controller struct {
gitspaceEventStore store.GitspaceEventStore
tx dbtx.Transactor
statefulLogger *logutil.StatefulLogger
scm scm.SCM
}
func NewController(
@ -47,6 +49,7 @@ func NewController(
orchestrator orchestrator.Orchestrator,
gitspaceEventStore store.GitspaceEventStore,
statefulLogger *logutil.StatefulLogger,
scm scm.SCM,
) *Controller {
return &Controller{
tx: tx,
@ -59,5 +62,6 @@ func NewController(
orchestrator: orchestrator,
gitspaceEventStore: gitspaceEventStore,
statefulLogger: statefulLogger,
scm: scm,
}
}

View File

@ -0,0 +1,52 @@
// 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 gitspace
import (
"context"
"fmt"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/types/enum"
)
type LookupRepoInput struct {
Identifier string `json:"-"`
SpaceRef string `json:"space_ref"` // Ref of the parent space
URL string `json:"url"`
}
func (c *Controller) LookupRepo(
ctx context.Context,
session *auth.Session,
in *LookupRepoInput,
) (*scm.CodeRepositoryResponse, error) {
space, err := c.spaceStore.FindByRef(ctx, in.SpaceRef)
if err != nil {
return nil, fmt.Errorf("failed to find space: %w", err)
}
err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, "", enum.PermissionGitspaceEdit)
if err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err)
}
repositoryRequest := scm.CodeRepositoryRequest{URL: in.URL}
codeRepositoryResponse, err := c.scm.CheckValidCodeRepo(ctx, repositoryRequest)
if err != nil {
return nil, err
}
return codeRepositoryResponse, nil
}

View File

@ -19,6 +19,7 @@ import (
gitspaceevents "github.com/harness/gitness/app/events/gitspace"
"github.com/harness/gitness/app/gitspace/logutil"
"github.com/harness/gitness/app/gitspace/orchestrator"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/store/database/dbtx"
@ -41,6 +42,7 @@ func ProvideController(
orchestrator orchestrator.Orchestrator,
eventStore store.GitspaceEventStore,
statefulLogger *logutil.StatefulLogger,
scm scm.SCM,
) *Controller {
return NewController(
tx,
@ -53,5 +55,6 @@ func ProvideController(
orchestrator,
eventStore,
statefulLogger,
scm,
)
}

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 gitspace
import (
"encoding/json"
"net/http"
"github.com/harness/gitness/app/api/controller/gitspace"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
)
func HandleLookupRepo(gitspaceCtrl *gitspace.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
in := new(gitspace.LookupRepoInput)
err := json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
return
}
repositoryResponse, err := gitspaceCtrl.LookupRepo(ctx, session, in)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.JSON(w, http.StatusOK, repositoryResponse)
}
}

View File

@ -19,6 +19,7 @@ import (
"github.com/harness/gitness/app/api/controller/gitspace"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/gitspace/scm"
"github.com/harness/gitness/livelog"
"github.com/harness/gitness/types"
@ -29,6 +30,10 @@ type createGitspaceRequest struct {
gitspace.CreateInput
}
type lookupRepoGitspaceRequest struct {
gitspace.LookupRepoInput
}
type updateGitspaceRequest struct {
}
@ -138,4 +143,16 @@ func gitspaceOperations(reflector *openapi3.Reflector) {
_ = reflector.SetJSONResponse(&opStreamLogs, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&opStreamLogs, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodGet, "/gitspaces/{gitspace_identifier}/logs/stream", opStreamLogs)
opRepoLookup := openapi3.Operation{}
opRepoLookup.WithTags("gitspaces")
opRepoLookup.WithSummary("Validate git repo for gitspaces")
opRepoLookup.WithMapOfAnything(map[string]interface{}{"operationId": "repoLookupForGitspace"})
_ = reflector.SetRequest(&opCreate, new(lookupRepoGitspaceRequest), http.MethodPost)
_ = reflector.SetJSONResponse(&opRepoLookup, new(scm.CodeRepositoryResponse), http.StatusCreated)
_ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusBadRequest)
_ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusForbidden)
_ = reflector.Spec.AddOperation(http.MethodPost, "/gitspaces/lookup-repo", opCreate)
}

View File

@ -70,11 +70,11 @@ func (o orchestrator) StartGitspace(
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerFailed)
log.Warn().Err(err).Msg("devcontainerConfig fetch failed.")
log.Warn().Err(err).Msg("devcontainer config fetch failed.")
}
if devcontainerConfig == nil {
log.Warn().Err(err).Msg("devcontainerConfig is nil, using empty config")
log.Warn().Err(err).Msg("devcontainer config is nil, using empty config")
devcontainerConfig = &types.DevcontainerConfig{}
}
@ -82,7 +82,7 @@ func (o orchestrator) StartGitspace(
infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID)
if err != nil {
return gitspaceInstance, fmt.Errorf("cannot get the infraProviderResource for ID %d: %w",
return gitspaceInstance, fmt.Errorf("cannot get the infraprovider resource for ID %d: %w",
gitspaceConfig.InfraProviderResourceID, err)
}

View File

@ -0,0 +1,25 @@
// 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 scm
type CodeRepositoryRequest struct {
URL string `json:"url"`
}
type CodeRepositoryResponse struct {
URL string `json:"url"`
Branch string `json:"branch,omitempty"`
CodeRepoIsPrivate bool `json:"is_private"`
}

View File

@ -18,6 +18,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
@ -32,6 +33,10 @@ import (
"github.com/rs/zerolog/log"
)
var (
ErrNoDefaultBranch = errors.New("no default branch")
)
var _ SCM = (*scm)(nil)
type SCM interface {
@ -39,10 +44,37 @@ type SCM interface {
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)
// check if the current URL is a valid and accessible code repo, input can be connector info, user token etc.
CheckValidCodeRepo(ctx context.Context, request CodeRepositoryRequest,
) (*CodeRepositoryResponse, error)
}
type scm struct{}
func (s scm) CheckValidCodeRepo(ctx context.Context, request CodeRepositoryRequest) (*CodeRepositoryResponse, error) {
err := validateURL(request)
if err != nil {
return nil, fmt.Errorf("invalid URL, %w", err)
}
codeRepositoryResponse := &CodeRepositoryResponse{
URL: request.URL,
CodeRepoIsPrivate: true,
}
defaultBranch, err := detectDefaultGitBranch(ctx, request.URL)
if err == nil {
branch := "main"
if defaultBranch != "" {
branch = defaultBranch
}
codeRepositoryResponse = &CodeRepositoryResponse{
URL: request.URL,
Branch: branch,
CodeRepoIsPrivate: false,
}
}
return codeRepositoryResponse, nil
}
func NewSCM() SCM {
return &scm{}
}
@ -158,6 +190,32 @@ func removeComments(input []byte) []byte {
return lineCommentRegex.ReplaceAll(input, nil)
}
func detectDefaultGitBranch(ctx context.Context, gitRepoDir string) (string, error) {
cmd := command.New("ls-remote",
command.WithFlag("--symref"),
command.WithFlag("-q"),
command.WithArg(gitRepoDir),
command.WithArg("HEAD"),
)
output := &bytes.Buffer{}
if err := cmd.Run(ctx, command.WithStdout(output)); err != nil {
return "", fmt.Errorf("failed to ls remote repo")
}
var lsRemoteHeadRegexp = regexp.MustCompile(`ref: refs/heads/([^\s]+)\s+HEAD`)
match := lsRemoteHeadRegexp.FindStringSubmatch(strings.TrimSpace(output.String()))
if match == nil {
return "", ErrNoDefaultBranch
}
return match[1], nil
}
func validateURL(request CodeRepositoryRequest) error {
if _, err := url.ParseRequestURI(request.URL); err != nil {
return err
}
return nil
}
func validateArgs(_ *types.GitspaceConfig) error {
// TODO Validate the args
return nil

View File

@ -709,6 +709,7 @@ func setupKeywordSearch(r chi.Router, searchCtrl *keywordsearch.Controller) {
func setupGitspaces(r chi.Router, gitspacesCtrl *gitspace.Controller) {
r.Route("/gitspaces", func(r chi.Router) {
r.Post("/lookup-repo", handlergitspace.HandleLookupRepo(gitspacesCtrl))
r.Post("/", handlergitspace.HandleCreateConfig(gitspacesCtrl))
r.Route(fmt.Sprintf("/{%s}", request.PathParamGitspaceIdentifier), func(r chi.Router) {
r.Get("/", handlergitspace.HandleFind(gitspacesCtrl))

View File

@ -116,7 +116,6 @@ func (g gitspaceEventStore) List(
queryStmt := database.Builder.
Select(gitspaceEventsColumnsWithID).
From(gitspaceEventsTable)
queryStmt = g.setQueryFilter(queryStmt, filter)
queryStmt = g.setSortFilter(queryStmt, filter)
@ -177,7 +176,7 @@ func (g gitspaceEventStore) setSortFilter(
stmt squirrel.SelectBuilder,
_ *types.GitspaceEventFilter,
) squirrel.SelectBuilder {
return stmt.OrderBy("geven_created ASC")
return stmt.OrderBy("geven_created DESC")
}
func (g gitspaceEventStore) setPaginationFilter(

View File

@ -343,7 +343,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
containerOrchestrator := container.ProvideEmbeddedDockerOrchestrator(dockerClientFactory, vsCode, vsCodeWeb, containerConfig, statefulLogger)
orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, infraProviderResourceStore, infraProvisioner, containerOrchestrator, reporter3)
gitspaceEventStore := database.ProvideGitspaceEventStore(db)
gitspaceController := gitspace.ProvideController(transactor, authorizer, infraProviderResourceStore, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, reporter3, orchestratorOrchestrator, gitspaceEventStore, statefulLogger)
gitspaceController := gitspace.ProvideController(transactor, authorizer, infraProviderResourceStore, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, reporter3, orchestratorOrchestrator, gitspaceEventStore, statefulLogger, scmSCM)
migrateController := migrate.ProvideController(authorizer, principalStore)
apiHandler := router.ProvideAPIHandler(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController, infraproviderController, gitspaceController, migrateController)
gitHandler := router.ProvideGitHandler(provider, authenticator, repoController)