Skip to content

Commit bf1afd3

Browse files
authored
feat(ui): add ascode environment edition in UI (#5269)
1 parent 0c4a64e commit bf1afd3

38 files changed

+1323
-706
lines changed

engine/api/api_routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ func (api *API) InitRouter() {
286286
r.Handle("/project/{permProjectKey}/environment/import", Scope(sdk.AuthConsumerScopeProject), r.POST(api.importNewEnvironmentHandler, DEPRECATED))
287287
r.Handle("/project/{permProjectKey}/environment/import/{environmentName}", Scope(sdk.AuthConsumerScopeProject), r.POST(api.importIntoEnvironmentHandler, DEPRECATED))
288288
r.Handle("/project/{permProjectKey}/environment/{environmentName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getEnvironmentHandler), r.PUT(api.updateEnvironmentHandler), r.DELETE(api.deleteEnvironmentHandler))
289+
r.Handle("/project/{permProjectKey}/environment/{environmentName}/ascode", Scope(sdk.AuthConsumerScopeProject), r.PUT(api.updateAsCodeEnvironmentHandler))
289290
r.Handle("/project/{permProjectKey}/environment/{environmentName}/usage", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getEnvironmentUsageHandler))
290291
r.Handle("/project/{permProjectKey}/environment/{environmentName}/keys", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getKeysInEnvironmentHandler), r.POST(api.addKeyInEnvironmentHandler))
291292
r.Handle("/project/{permProjectKey}/environment/{environmentName}/keys/{name}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteKeyInEnvironmentHandler))

engine/api/application.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,27 @@ func (api *API) getApplicationHandler() service.Handler {
151151
}
152152

153153
if app.FromRepository != "" {
154-
proj, err := project.Load(api.mustDB(), projectKey)
154+
proj, err := project.Load(api.mustDB(), projectKey,
155+
project.LoadOptions.WithApplicationWithDeploymentStrategies,
156+
project.LoadOptions.WithPipelines,
157+
project.LoadOptions.WithEnvironments,
158+
project.LoadOptions.WithIntegrations)
155159
if err != nil {
156160
return err
157161
}
162+
158163
wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, app.FromRepository, workflow.LoadOptions{
159164
WithTemplate: true,
160165
})
161166
if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
162-
return sdk.NewErrorFrom(err, "cannot found workflow holder of the pipeline")
167+
return sdk.NewErrorFrom(err, "cannot found workflow holder of the application")
163168
}
164169
app.WorkflowAscodeHolder = wkAscodeHolder
170+
171+
// FIXME from_repository should never be set if the workflow holder was deleted
172+
if app.WorkflowAscodeHolder == nil {
173+
app.FromRepository = ""
174+
}
165175
}
166176

167177
return service.WriteJSON(w, app, http.StatusOK)

engine/api/ascode/pull_request.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
PipelineEvent EventType = "pipeline"
2424
WorkflowEvent EventType = "workflow"
2525
ApplicationEvent EventType = "application"
26+
EnvironmentEvent EventType = "environment"
2627
)
2728

2829
type EntityData struct {
@@ -200,6 +201,20 @@ func createPullRequest(ctx context.Context, db *gorp.DbMap, store cache.Store, p
200201
if !found {
201202
asCodeEvent.Data.Applications[ed.ID] = ed.Name
202203
}
204+
case EnvironmentEvent:
205+
if asCodeEvent.Data.Environments == nil {
206+
asCodeEvent.Data.Environments = make(map[int64]string)
207+
}
208+
found := false
209+
for k := range asCodeEvent.Data.Environments {
210+
if k == ed.ID {
211+
found = true
212+
break
213+
}
214+
}
215+
if !found {
216+
asCodeEvent.Data.Environments[ed.ID] = ed.Name
217+
}
203218
}
204219

205220
if err := UpsertEvent(db, asCodeEvent); err != nil {

engine/api/environment.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ package api
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67

78
"github.com/go-gorp/gorp"
89
"github.com/gorilla/mux"
910

11+
"github.com/ovh/cds/engine/api/application"
12+
"github.com/ovh/cds/engine/api/ascode"
1013
"github.com/ovh/cds/engine/api/environment"
1114
"github.com/ovh/cds/engine/api/event"
15+
"github.com/ovh/cds/engine/api/keys"
16+
"github.com/ovh/cds/engine/api/operation"
1217
"github.com/ovh/cds/engine/api/project"
1318
"github.com/ovh/cds/engine/api/workflow"
1419
"github.com/ovh/cds/engine/service"
1520
"github.com/ovh/cds/sdk"
21+
"github.com/ovh/cds/sdk/exportentities"
1622
"github.com/ovh/cds/sdk/log"
1723
)
1824

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

82+
if env.FromRepository != "" {
83+
proj, err := project.Load(api.mustDB(), projectKey,
84+
project.LoadOptions.WithApplicationWithDeploymentStrategies,
85+
project.LoadOptions.WithPipelines,
86+
project.LoadOptions.WithEnvironments,
87+
project.LoadOptions.WithIntegrations)
88+
if err != nil {
89+
return err
90+
}
91+
92+
wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, env.FromRepository, workflow.LoadOptions{
93+
WithTemplate: true,
94+
})
95+
if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
96+
return sdk.NewErrorFrom(err, "cannot found workflow holder of the environment")
97+
}
98+
env.WorkflowAscodeHolder = wkAscodeHolder
99+
100+
// FIXME from_repository should never be set if the workflow holder was deleted
101+
if env.WorkflowAscodeHolder == nil {
102+
env.FromRepository = ""
103+
}
104+
}
105+
76106
return service.WriteJSON(w, env, http.StatusOK)
77107
}
78108
}
@@ -194,6 +224,111 @@ func (api *API) deleteEnvironmentHandler() service.Handler {
194224
}
195225
}
196226

227+
func (api *API) updateAsCodeEnvironmentHandler() service.Handler {
228+
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
229+
// Get pipeline and action name in URL
230+
vars := mux.Vars(r)
231+
key := vars[permProjectKey]
232+
environmentName := vars["environmentName"]
233+
234+
branch := FormString(r, "branch")
235+
message := FormString(r, "message")
236+
237+
if branch == "" || message == "" {
238+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing branch or message data")
239+
}
240+
241+
var env sdk.Environment
242+
if err := service.UnmarshalBody(r, &env); err != nil {
243+
return err
244+
}
245+
246+
// check application name pattern
247+
regexp := sdk.NamePatternRegex
248+
if !regexp.MatchString(env.Name) {
249+
return sdk.WrapError(sdk.ErrInvalidApplicationPattern, "Environment name %s do not respect pattern", env.Name)
250+
}
251+
252+
proj, err := project.Load(api.mustDB(), key, project.LoadOptions.WithClearKeys)
253+
if err != nil {
254+
return err
255+
}
256+
257+
envDB, err := environment.LoadEnvironmentByName(api.mustDB(), key, environmentName)
258+
if err != nil {
259+
return sdk.WrapError(err, "cannot load environment %s", environmentName)
260+
}
261+
262+
if envDB.FromRepository == "" {
263+
return sdk.NewErrorFrom(sdk.ErrForbidden, "current environment is not ascode")
264+
}
265+
266+
wkHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, envDB.FromRepository, workflow.LoadOptions{
267+
WithTemplate: true,
268+
})
269+
if err != nil {
270+
return err
271+
}
272+
if wkHolder.TemplateInstance != nil {
273+
return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot edit an application that was generated by a template")
274+
}
275+
276+
var rootApp *sdk.Application
277+
if wkHolder.WorkflowData.Node.Context != nil && wkHolder.WorkflowData.Node.Context.ApplicationID != 0 {
278+
rootApp, err = application.LoadByIDWithClearVCSStrategyPassword(api.mustDB(), wkHolder.WorkflowData.Node.Context.ApplicationID)
279+
if err != nil {
280+
return err
281+
}
282+
}
283+
if rootApp == nil {
284+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot find the root application of the workflow %s that hold the pipeline", wkHolder.Name)
285+
}
286+
287+
// create keys
288+
for i := range env.Keys {
289+
k := &env.Keys[i]
290+
newKey, err := keys.GenerateKey(k.Name, k.Type)
291+
if err != nil {
292+
return err
293+
}
294+
k.Public = newKey.Public
295+
k.Private = newKey.Private
296+
k.KeyID = newKey.KeyID
297+
}
298+
299+
u := getAPIConsumer(ctx)
300+
env.ProjectID = proj.ID
301+
envExported, err := environment.ExportEnvironment(api.mustDB(), env, project.EncryptWithBuiltinKey, fmt.Sprintf("env:%d:%s", envDB.ID, branch))
302+
if err != nil {
303+
return err
304+
}
305+
wp := exportentities.WorkflowComponents{
306+
Environments: []exportentities.Environment{envExported},
307+
}
308+
309+
ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *proj, wp, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u)
310+
if err != nil {
311+
return err
312+
}
313+
314+
sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodeEnvironmentHandler-%s", ope.UUID), func(ctx context.Context) {
315+
ed := ascode.EntityData{
316+
FromRepo: envDB.FromRepository,
317+
Type: ascode.EnvironmentEvent,
318+
ID: envDB.ID,
319+
Name: envDB.Name,
320+
OperationUUID: ope.UUID,
321+
}
322+
asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *proj, wkHolder.ID, *rootApp, ed, u)
323+
if asCodeEvent != nil {
324+
event.PublishAsCodeEvent(ctx, proj.Key, *asCodeEvent, u)
325+
}
326+
}, api.PanicDump())
327+
328+
return service.WriteJSON(w, ope, http.StatusOK)
329+
}
330+
}
331+
197332
func (api *API) updateEnvironmentHandler() service.Handler {
198333
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
199334
// Get pipeline and action name in URL

engine/api/environment/environment_exporter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ func Export(ctx context.Context, db gorp.SqlExecutor, key string, envName string
3232
}
3333
env.Keys = keys
3434

35-
return ExportEnvironment(db, *env, encryptFunc)
35+
return ExportEnvironment(db, *env, encryptFunc, fmt.Sprintf("env:%d", env.ID))
3636
}
3737

3838
// ExportEnvironment encrypt and export
39-
func ExportEnvironment(db gorp.SqlExecutor, env sdk.Environment, encryptFunc sdk.EncryptFunc) (exportentities.Environment, error) {
39+
func ExportEnvironment(db gorp.SqlExecutor, env sdk.Environment, encryptFunc sdk.EncryptFunc, encryptPrefix string) (exportentities.Environment, error) {
4040
var envvars []sdk.EnvironmentVariable
4141
for _, v := range env.Variables {
4242
switch v.Type {

0 commit comments

Comments
 (0)