Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add ascode environment edition in UI #5269

Merged
merged 16 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ func (api *API) InitRouter() {
r.Handle("/project/{permProjectKey}/environment/import", Scope(sdk.AuthConsumerScopeProject), r.POST(api.importNewEnvironmentHandler, DEPRECATED))
r.Handle("/project/{permProjectKey}/environment/import/{environmentName}", Scope(sdk.AuthConsumerScopeProject), r.POST(api.importIntoEnvironmentHandler, DEPRECATED))
r.Handle("/project/{permProjectKey}/environment/{environmentName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getEnvironmentHandler), r.PUT(api.updateEnvironmentHandler), r.DELETE(api.deleteEnvironmentHandler))
r.Handle("/project/{permProjectKey}/environment/{environmentName}/ascode", Scope(sdk.AuthConsumerScopeProject), r.PUT(api.updateAsCodeEnvironmentHandler))
r.Handle("/project/{permProjectKey}/environment/{environmentName}/usage", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getEnvironmentUsageHandler))
r.Handle("/project/{permProjectKey}/environment/{environmentName}/keys", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getKeysInEnvironmentHandler), r.POST(api.addKeyInEnvironmentHandler))
r.Handle("/project/{permProjectKey}/environment/{environmentName}/keys/{name}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteKeyInEnvironmentHandler))
Expand Down
14 changes: 12 additions & 2 deletions engine/api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,27 @@ func (api *API) getApplicationHandler() service.Handler {
}

if app.FromRepository != "" {
proj, err := project.Load(api.mustDB(), projectKey)
proj, err := project.Load(api.mustDB(), projectKey,
project.LoadOptions.WithApplicationWithDeploymentStrategies,
project.LoadOptions.WithPipelines,
project.LoadOptions.WithEnvironments,
project.LoadOptions.WithIntegrations)
if err != nil {
return err
}

wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, app.FromRepository, workflow.LoadOptions{
WithTemplate: true,
})
if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
return sdk.NewErrorFrom(err, "cannot found workflow holder of the pipeline")
return sdk.NewErrorFrom(err, "cannot found workflow holder of the application")
}
app.WorkflowAscodeHolder = wkAscodeHolder

// FIXME from_repository should never be set if the workflow holder was deleted
if app.WorkflowAscodeHolder == nil {
app.FromRepository = ""
}
}

return service.WriteJSON(w, app, http.StatusOK)
Expand Down
15 changes: 15 additions & 0 deletions engine/api/ascode/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
PipelineEvent EventType = "pipeline"
WorkflowEvent EventType = "workflow"
ApplicationEvent EventType = "application"
EnvironmentEvent EventType = "environment"
)

type EntityData struct {
Expand Down Expand Up @@ -199,6 +200,20 @@ func createPullRequest(ctx context.Context, db *gorp.DbMap, store cache.Store, p
if !found {
asCodeEvent.Data.Applications[ed.ID] = ed.Name
}
case EnvironmentEvent:
if asCodeEvent.Data.Environments == nil {
asCodeEvent.Data.Environments = make(map[int64]string)
}
found := false
for k := range asCodeEvent.Data.Environments {
if k == ed.ID {
found = true
break
}
}
if !found {
asCodeEvent.Data.Environments[ed.ID] = ed.Name
}
}

if err := UpsertEvent(db, asCodeEvent); err != nil {
Expand Down
132 changes: 132 additions & 0 deletions engine/api/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ package api

import (
"context"
"fmt"
"net/http"

"github.com/go-gorp/gorp"
"github.com/gorilla/mux"

"github.com/ovh/cds/engine/api/application"
"github.com/ovh/cds/engine/api/ascode"
"github.com/ovh/cds/engine/api/environment"
"github.com/ovh/cds/engine/api/event"
"github.com/ovh/cds/engine/api/keys"
"github.com/ovh/cds/engine/api/operation"
"github.com/ovh/cds/engine/api/project"
"github.com/ovh/cds/engine/api/workflow"
"github.com/ovh/cds/engine/service"
"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/exportentities"
"github.com/ovh/cds/sdk/log"
)

Expand Down Expand Up @@ -73,6 +79,30 @@ func (api *API) getEnvironmentHandler() service.Handler {
env.Usage.Workflows = wf
}

if env.FromRepository != "" {
proj, err := project.Load(api.mustDB(), projectKey,
project.LoadOptions.WithApplicationWithDeploymentStrategies,
project.LoadOptions.WithPipelines,
project.LoadOptions.WithEnvironments,
project.LoadOptions.WithIntegrations)
if err != nil {
return err
}

wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, env.FromRepository, workflow.LoadOptions{
WithTemplate: true,
})
if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
return sdk.NewErrorFrom(err, "cannot found workflow holder of the environment")
}
env.WorkflowAscodeHolder = wkAscodeHolder

// FIXME from_repository should never be set if the workflow holder was deleted
if env.WorkflowAscodeHolder == nil {
env.FromRepository = ""
}
}

return service.WriteJSON(w, env, http.StatusOK)
}
}
Expand Down Expand Up @@ -194,6 +224,108 @@ func (api *API) deleteEnvironmentHandler() service.Handler {
}
}

func (api *API) updateAsCodeEnvironmentHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
// Get pipeline and action name in URL
vars := mux.Vars(r)
key := vars[permProjectKey]
environmentName := vars["environmentName"]

branch := FormString(r, "branch")
message := FormString(r, "message")

if branch == "" || message == "" {
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing branch or message data")
}

var env sdk.Environment
if err := service.UnmarshalBody(r, &env); err != nil {
return err
}

// check application name pattern
regexp := sdk.NamePatternRegex
if !regexp.MatchString(env.Name) {
return sdk.WrapError(sdk.ErrInvalidApplicationPattern, "Environment name %s do not respect pattern", env.Name)
}

proj, err := project.Load(api.mustDB(), key, project.LoadOptions.WithClearKeys)
if err != nil {
return err
}

envDB, err := environment.LoadEnvironmentByName(api.mustDB(), key, environmentName)
if err != nil {
return sdk.WrapError(err, "cannot load environment %s", environmentName)
}

if envDB.FromRepository == "" {
return sdk.NewErrorFrom(sdk.ErrForbidden, "current environment is not ascode")
}

wkHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, envDB.FromRepository, workflow.LoadOptions{
WithTemplate: true,
})
if err != nil {
return err
}
if wkHolder.TemplateInstance != nil {
return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot edit an application that was generated by a template")
}

var rootApp *sdk.Application
if wkHolder.WorkflowData.Node.Context != nil && wkHolder.WorkflowData.Node.Context.ApplicationID != 0 {
rootApp, err = application.LoadByIDWithClearVCSStrategyPassword(api.mustDB(), wkHolder.WorkflowData.Node.Context.ApplicationID)
if err != nil {
return err
}
}
if rootApp == nil {
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot find the root application of the workflow %s that hold the pipeline", wkHolder.Name)
}

// create keys
for i := range env.Keys {
k := &env.Keys[i]
newKey, err := keys.GenerateKey(k.Name, k.Type)
if err != nil {
return err
}
k.Public = newKey.Public
k.Private = newKey.Private
k.KeyID = newKey.KeyID
}

u := getAPIConsumer(ctx)
env.ProjectID = proj.ID
envExported, err := environment.ExportEnvironment(api.mustDB(), env, project.EncryptWithBuiltinKey, fmt.Sprintf("env:%d:%s", envDB.ID, branch))
wp := exportentities.WorkflowComponents{
Environments: []exportentities.Environment{envExported},
}

sguiheux marked this conversation as resolved.
Show resolved Hide resolved
ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *proj, wp, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u)
if err != nil {
return err
}

sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodeEnvironmentHandler-%s", ope.UUID), func(ctx context.Context) {
ed := ascode.EntityData{
FromRepo: envDB.FromRepository,
Type: ascode.EnvironmentEvent,
ID: envDB.ID,
Name: envDB.Name,
OperationUUID: ope.UUID,
}
asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *proj, wkHolder.ID, *rootApp, ed, u)
if asCodeEvent != nil {
event.PublishAsCodeEvent(ctx, proj.Key, *asCodeEvent, u)
}
}, api.PanicDump())

return service.WriteJSON(w, ope, http.StatusOK)
}
}

func (api *API) updateEnvironmentHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
// Get pipeline and action name in URL
Expand Down
4 changes: 2 additions & 2 deletions engine/api/environment/environment_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ func Export(ctx context.Context, db gorp.SqlExecutor, key string, envName string
}
env.Keys = keys

return ExportEnvironment(db, *env, encryptFunc)
return ExportEnvironment(db, *env, encryptFunc, fmt.Sprintf("env:%d", env.ID))
}

// ExportEnvironment encrypt and export
func ExportEnvironment(db gorp.SqlExecutor, env sdk.Environment, encryptFunc sdk.EncryptFunc) (exportentities.Environment, error) {
func ExportEnvironment(db gorp.SqlExecutor, env sdk.Environment, encryptFunc sdk.EncryptFunc, encryptPrefix string) (exportentities.Environment, error) {
var envvars []sdk.EnvironmentVariable
for _, v := range env.Variables {
switch v.Type {
Expand Down
Loading