diff --git a/momentum-core/artefacts/artefact-tree.go b/momentum-core/artefacts/artefact-tree.go index 7e2b7d1..efcc76f 100644 --- a/momentum-core/artefacts/artefact-tree.go +++ b/momentum-core/artefacts/artefact-tree.go @@ -347,6 +347,7 @@ func loadArtefactTreeUntilDepth(path string, parent *Artefact, depth int) (*Arte if err != nil { return nil, err } + defer dir.Close() dirEntries, err := dir.ReadDir(-1) // reads all entries for _, entry := range dirEntries { @@ -372,6 +373,7 @@ func children(artefact *Artefact) ([]*Artefact, error) { if err != nil { return make([]*Artefact, 0), err } + defer dir.Close() dirEntries, err := dir.ReadDir(-1) // reads all entries for _, entry := range dirEntries { diff --git a/momentum-core/artefacts/override.go b/momentum-core/artefacts/override.go deleted file mode 100644 index 9f3d6b7..0000000 --- a/momentum-core/artefacts/override.go +++ /dev/null @@ -1,36 +0,0 @@ -package artefacts - -import ( - "strings" -) - -type OverwriteAdvice func(string) []*Artefact - -// extend this list to add more overwriting rules -var ActiveOverwriteAdvices []OverwriteAdvice = []OverwriteAdvice{overwritesByFilenamePriorityAsc} - -// gets all files which are higher up in the structure with the same name as the given file path. -func overwritesByFilenamePriorityAsc(path string) []*Artefact { - - overwritable := FindArtefactByPath(path) - if overwritable != nil { - - overwritesOrderedByPriorityAsc := make([]*Artefact, 0) - current := overwritable - for current != nil { - - for _, child := range current.Content { - - if strings.EqualFold(overwritable.Name, child.Name) && !strings.EqualFold(overwritable.Id, child.Id) { - overwritesOrderedByPriorityAsc = append(overwritesOrderedByPriorityAsc, child) - } - } - - current = current.Parent - } - - return overwritesOrderedByPriorityAsc - } - - return make([]*Artefact, 0) -} diff --git a/momentum-core/artefacts/routes.go b/momentum-core/artefacts/routes.go index 6f4f526..c213073 100644 --- a/momentum-core/artefacts/routes.go +++ b/momentum-core/artefacts/routes.go @@ -10,9 +10,9 @@ import ( func RegisterArtefactRoutes(engine *gin.Engine) { engine.GET(config.API_ARTEFACT_BY_ID, GetArtefact) - engine.GET(config.API_APPLICATIONS, GetApplications) - engine.GET(config.API_STAGES, GetStages) - engine.GET(config.API_DEPLOYMENTS, GetDeployments) + engine.GET(config.API_ARTEFACT_APPLICATIONS, GetApplications) + engine.GET(config.API_ARTEFACT_STAGES, GetStages) + engine.GET(config.API_ARTEFACT_DEPLOYMENTS, GetDeployments) } // GetArtefact godoc diff --git a/momentum-core/backtracking/backtrack.go b/momentum-core/backtracking/backtrack.go index 7d2cf83..c1c5087 100644 --- a/momentum-core/backtracking/backtrack.go +++ b/momentum-core/backtracking/backtrack.go @@ -78,7 +78,7 @@ func root[T any, P any](n *Node[T, P]) *Node[T, P] { func reject[T any, P any](search Search[T, P], n *Node[T, P]) bool { - // can be dangerous, because results are ommited possibly ommitted + // can be dangerous, because correct results are possibly ommitted return search.StopEarly(search.Predicate(), search.Comparable(n)) } diff --git a/momentum-core/config/config.go b/momentum-core/config/config.go index 8a147f4..63fd4c7 100644 --- a/momentum-core/config/config.go +++ b/momentum-core/config/config.go @@ -36,15 +36,9 @@ var LOGGER ILoggerClient type MomentumConfig struct { dataDir string validationTmpDir string - templatesDir string logDir string transactionToken *gittransaction.Token - - applicationTemplateFolderPath string - stageTemplateFolderPath string - deploymentTemplateFilePath string - deploymentTemplateFolderPath string } func (m *MomentumConfig) DataDir() string { @@ -52,65 +46,83 @@ func (m *MomentumConfig) DataDir() string { } func (m *MomentumConfig) RepoDir() string { - return filepath.Join(m.dataDir, "repository") + return filepath.Join(m.DataDir(), "repository") } func (m *MomentumConfig) ValidationTmpDir() string { return m.validationTmpDir } -func (m *MomentumConfig) TemplateDir() string { - return m.templatesDir -} - func (m *MomentumConfig) LogDir() string { return m.logDir } -func (m *MomentumConfig) ApplicationTemplateFolderPath() string { - return m.applicationTemplateFolderPath +func TemplateDir(config *MomentumConfig) string { + return filepath.Join(config.RepoDir(), "templates") } -func (m *MomentumConfig) StageTemplateFolderPath() string { - return m.stageTemplateFolderPath +func ApplicationTemplatesPath(config *MomentumConfig) string { + return filepath.Join(TemplateDir(config), "applications") } -func (m *MomentumConfig) DeploymentTemplateFolderPath() string { - return m.deploymentTemplateFolderPath +func StageTemplatesPath(config *MomentumConfig) string { + return filepath.Join(TemplateDir(config), "stages") } -func (m *MomentumConfig) DeploymentTemplateFilePath() string { - return m.deploymentTemplateFilePath +func DeploymentTemplatesPath(config *MomentumConfig) string { + return filepath.Join(TemplateDir(config), "deployments") } func (m *MomentumConfig) TransactionToken() *gittransaction.Token { return m.transactionToken } -func (m *MomentumConfig) checkMandatoryTemplates() error { +func checkMandatoryTemplates(config *MomentumConfig) error { + + errs := make([]error, 0) + + templatePath := TemplateDir(config) + if !utils.FileExists(templatePath) { + err := utils.DirCreate(templatePath) + if err != nil { + errs = append(errs, err) + } + } - if !utils.FileExists(m.ApplicationTemplateFolderPath()) { - return errors.New("provide mandatory template for application folder at " + m.TemplateDir()) + appTemplatePath := ApplicationTemplatesPath(config) + if !utils.FileExists(appTemplatePath) { + err := utils.DirCreate(appTemplatePath) + if err != nil { + errs = append(errs, err) + } } - if !utils.FileExists(m.StageTemplateFolderPath()) { - return errors.New("provide mandatory template for stage folder at " + m.TemplateDir()) + stageTemplatePath := StageTemplatesPath(config) + if !utils.FileExists(stageTemplatePath) { + err := utils.DirCreate(stageTemplatePath) + if err != nil { + errs = append(errs, err) + } } - if !utils.FileExists(m.DeploymentTemplateFolderPath()) { - return errors.New("provide mandatory template for deployment folders at " + m.DeploymentTemplateFolderPath()) + deploymentTemplatePath := DeploymentTemplatesPath(config) + if !utils.FileExists(deploymentTemplatePath) { + err := utils.DirCreate(deploymentTemplatePath) + if err != nil { + errs = append(errs, err) + } } - if !utils.FileExists(m.DeploymentTemplateFilePath()) { - return errors.New("provide mandatory template for deployment files at " + m.DeploymentTemplateFilePath()) + if len(errs) > 0 { + return errors.Join(errs...) } return nil } -func (m *MomentumConfig) initializeRepository() error { +func initializeRepository(config *MomentumConfig) error { - _, err := os.Stat(m.RepoDir()) + _, err := os.Stat(config.RepoDir()) if !os.IsNotExist(err) { LOGGER.LogInfo("will not clone repository because one present", "STARTUP") return nil @@ -121,7 +133,11 @@ func (m *MomentumConfig) initializeRepository() error { return errors.New("failed initializing momentum because " + MOMENTUM_GIT_REPO_URL + " was not set") } - cloneRepoTo(repoUrl, "", "", m.RepoDir()) + cloneRepoTo(repoUrl, "", "", config.RepoDir()) + + if !utils.FileExists(filepath.Join(config.RepoDir(), MOMENTUM_ROOT)) { + return errors.New("invalid momentum repository") + } return nil } @@ -159,7 +175,6 @@ func InitializeMomentumCore() (*MomentumConfig, error) { momentumDir := utils.BuildPath(usrHome, ".momentum") dataDir := utils.BuildPath(momentumDir, "data") validationTmpDir := utils.BuildPath(momentumDir, "validation") - templatesDir := utils.BuildPath(momentumDir, "templates") logDir := momentumDir createPathIfNotPresent(dataDir, momentumDir) @@ -170,25 +185,25 @@ func InitializeMomentumCore() (*MomentumConfig, error) { config.logDir = logDir config.dataDir = dataDir config.validationTmpDir = validationTmpDir - config.templatesDir = templatesDir - config.applicationTemplateFolderPath = utils.BuildPath(templatesDir, "applications") - config.stageTemplateFolderPath = utils.BuildPath(templatesDir, "stages") - config.deploymentTemplateFolderPath = utils.BuildPath(templatesDir, "deployments", "deploymentName") - config.deploymentTemplateFilePath = utils.BuildPath(templatesDir, "deployments", "deploymentName.yaml") + + LOGGER, err = NewLogger(config.LogDir()) + if err != nil { + panic("failed initializing logger: " + err.Error()) + } err = config.initializeGitAccessToken() if err != nil { return nil, err } - err = config.checkMandatoryTemplates() + err = initializeRepository(config) if err != nil { return nil, err } - LOGGER, err = NewLogger(config.LogDir()) + err = checkMandatoryTemplates(config) if err != nil { - panic("failed initializing logger: " + err.Error()) + return nil, err } GLOBAL = config diff --git a/momentum-core/config/endpoints.go b/momentum-core/config/endpoints.go new file mode 100644 index 0000000..e2ba0f7 --- /dev/null +++ b/momentum-core/config/endpoints.go @@ -0,0 +1,23 @@ +package config + +const API = "/api" +const API_VERSION_BETA = API + "/beta" + +const API_FILE_BY_ID = API_VERSION_BETA + "/file/:id" +const API_FILE_ADD = API_VERSION_BETA + "/file" +const API_FILE_UPDATE = API_VERSION_BETA + "/file/:id" +const API_FILE_LINE_OVERWRITTENBY = API_VERSION_BETA + "/file/:id/line/:lineNumber/overwritten-by" + +const API_ARTEFACT_BY_ID = API_VERSION_BETA + "/artefact/:id" +const API_ARTEFACT_APPLICATIONS = API_VERSION_BETA + "/applications" +const API_ARTEFACT_STAGES = API_VERSION_BETA + "/stages" +const API_ARTEFACT_DEPLOYMENTS = API_VERSION_BETA + "/deployments" + +const API_TEMPLATES_PREFIX = API_VERSION_BETA + "/templates" +const API_TEMPLATES_APPLICATIONS = API_TEMPLATES_PREFIX + "/applications" +const API_TEMPLATES_STAGES = API_TEMPLATES_PREFIX + "/stages" +const API_TEMPLATES_DEPLOYMENTS = API_TEMPLATES_PREFIX + "/deployments" +const API_TEMPLATES_ADD = API_TEMPLATES_PREFIX +const API_TEMPLATES_SPEC_PREFIX = API_TEMPLATES_PREFIX + "/spec" +const API_TEMPLATE_GET_SPEC = API_TEMPLATES_SPEC_PREFIX + "/:templateName" +const API_TEMPLATE_APPLY = API_TEMPLATES_SPEC_PREFIX + "/apply/:anchorArtefactId" diff --git a/momentum-core/config/routes.go b/momentum-core/config/routes.go deleted file mode 100644 index cb9e89b..0000000 --- a/momentum-core/config/routes.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -const API = "/api" -const API_VERSION_BETA = API + "/beta" - -const API_FILE_BY_ID = API_VERSION_BETA + "/file/:id" -const API_FILE_ADD = API_VERSION_BETA + "/file" -const API_DIR_BY_ID = API_VERSION_BETA + "/dir/:id" -const API_FILE_LINE_OVERWRITTENBY = API_VERSION_BETA + "/file/:id/line/:lineNumber/overwritten-by" -const API_FILE_LINE_OVERWRITES = API_VERSION_BETA + "/file/:id/line/:lineNumber/overwrites" - -const API_ARTEFACT_BY_ID = API_VERSION_BETA + "/artefact/:id" -const API_APPLICATIONS = API_VERSION_BETA + "/applications" -const API_STAGES = API_VERSION_BETA + "/stages" -const API_DEPLOYMENTS = API_VERSION_BETA + "/deployments" diff --git a/momentum-core/docs/docs.go b/momentum-core/docs/docs.go index 6e70d86..6402ee0 100644 --- a/momentum-core/docs/docs.go +++ b/momentum-core/docs/docs.go @@ -162,7 +162,18 @@ const docTemplate = `{ "tags": [ "files" ], - "summary": "adds a new file to a given parent", + "summary": "adds a new file to a given parent (triggers transaction)", + "parameters": [ + { + "description": "the body shall contain a File instance", + "name": "CreateFileRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/files.CreateFileRequest" + } + } + ], "responses": { "200": { "description": "OK", @@ -235,6 +246,62 @@ const docTemplate = `{ } } } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "updates the given file (triggers transaction)", + "parameters": [ + { + "type": "string", + "description": "file id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "the body shall contain a File instance", + "name": "File", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/files.File" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/files.File" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } } }, "/api/beta/file/{id}/line/{lineNumber}/overwritten-by": { @@ -245,7 +312,7 @@ const docTemplate = `{ "tags": [ "files" ], - "summary": "gets a list of properties which overwrite the given line.", + "summary": "gets a list of overwrites which overwrite the given line.", "parameters": [ { "type": "string", @@ -268,7 +335,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/files.Overwrite" + "$ref": "#/definitions/overwrites.Overwrite" } } }, @@ -338,6 +405,281 @@ const docTemplate = `{ } } } + }, + "/api/beta/templates": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "adds a new template (triggers transaction)", + "parameters": [ + { + "description": "the body shall contain a CreateTemplateRequest instance", + "name": "CreateTemplateRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/templates.CreateTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/applications": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available application templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/deployments": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available deployment templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/spec/:templateName": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets the spec for a template, which contains values to be set when applying the template", + "parameters": [ + { + "type": "string", + "description": "name of the template (template names are unique)", + "name": "templateName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/spec/apply/:anchorArtefactId": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets the spec for a template, which contains values to be set when applying the template (triggers transaction)", + "parameters": [ + { + "type": "string", + "description": "id of the artefact where the template shall be applied. Must be a directory.", + "name": "anchorArtefactId", + "in": "path", + "required": true + }, + { + "description": "the body shall contain a CreateTemplateRequest instance", + "name": "TemplateSpec", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/templates.TemplateSpec" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/stages": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available stage templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } } }, "definitions": { @@ -406,6 +748,43 @@ const docTemplate = `{ } } }, + "files.CreateFileRequest": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + } + } + }, + "files.Dir": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/files.File" + } + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "subDirs": { + "type": "array", + "items": { + "$ref": "#/definitions/files.Dir" + } + } + } + }, "files.File": { "type": "object", "properties": { @@ -420,7 +799,7 @@ const docTemplate = `{ } } }, - "files.Overwrite": { + "overwrites.Overwrite": { "type": "object", "properties": { "originFileId": { @@ -436,6 +815,137 @@ const docTemplate = `{ "type": "integer" } } + }, + "templates.CreateTemplateRequest": { + "type": "object", + "properties": { + "template": { + "description": "the toplevel directories name is the name of the template", + "allOf": [ + { + "$ref": "#/definitions/templates.TemplateDir" + } + ] + }, + "templateConfig": { + "$ref": "#/definitions/templates.TemplateConfig" + }, + "templateKind": { + "$ref": "#/definitions/templates.TemplateKind" + } + } + }, + "templates.Template": { + "type": "object", + "properties": { + "children": { + "description": "The children are templates which are contained within the template.", + "type": "array", + "items": { + "$ref": "#/definitions/templates.Template" + } + }, + "kind": { + "$ref": "#/definitions/templates.TemplateKind" + }, + "name": { + "description": "be aware, that each template must have an unique name\nit doesn't matter if they are of different template kind", + "type": "string" + }, + "root": { + "$ref": "#/definitions/files.Dir" + } + } + }, + "templates.TemplateConfig": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "type": "string" + } + }, + "kind": { + "$ref": "#/definitions/templates.TemplateKind" + } + } + }, + "templates.TemplateDir": { + "type": "object", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.TemplateDir" + } + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.TemplateFile" + } + }, + "name": { + "type": "string" + } + } + }, + "templates.TemplateFile": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "templateBody": { + "description": "base64 encoded", + "type": "string" + } + } + }, + "templates.TemplateKind": { + "type": "integer", + "enum": [ + 1, + 2, + 4 + ], + "x-enum-varnames": [ + "APPLICATION", + "STAGE", + "DEPLOYMENT" + ] + }, + "templates.TemplateSpec": { + "type": "object", + "properties": { + "template": { + "$ref": "#/definitions/templates.Template" + }, + "valueSpecs": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.ValueSpec" + } + } + } + }, + "templates.ValueSpec": { + "type": "object", + "properties": { + "name": { + "description": "name of the value (name displayed in frontend)", + "type": "string" + }, + "templateName": { + "description": "name of the template which the value belongs to", + "type": "string" + }, + "value": { + "description": "the value assigned", + "type": "string" + } + } } } }` diff --git a/momentum-core/docs/swagger.json b/momentum-core/docs/swagger.json index 25c7054..d05e863 100644 --- a/momentum-core/docs/swagger.json +++ b/momentum-core/docs/swagger.json @@ -159,7 +159,18 @@ "tags": [ "files" ], - "summary": "adds a new file to a given parent", + "summary": "adds a new file to a given parent (triggers transaction)", + "parameters": [ + { + "description": "the body shall contain a File instance", + "name": "CreateFileRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/files.CreateFileRequest" + } + } + ], "responses": { "200": { "description": "OK", @@ -232,6 +243,62 @@ } } } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "updates the given file (triggers transaction)", + "parameters": [ + { + "type": "string", + "description": "file id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "the body shall contain a File instance", + "name": "File", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/files.File" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/files.File" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } } }, "/api/beta/file/{id}/line/{lineNumber}/overwritten-by": { @@ -242,7 +309,7 @@ "tags": [ "files" ], - "summary": "gets a list of properties which overwrite the given line.", + "summary": "gets a list of overwrites which overwrite the given line.", "parameters": [ { "type": "string", @@ -265,7 +332,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/files.Overwrite" + "$ref": "#/definitions/overwrites.Overwrite" } } }, @@ -335,6 +402,281 @@ } } } + }, + "/api/beta/templates": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "adds a new template (triggers transaction)", + "parameters": [ + { + "description": "the body shall contain a CreateTemplateRequest instance", + "name": "CreateTemplateRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/templates.CreateTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/applications": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available application templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/deployments": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available deployment templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/spec/:templateName": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets the spec for a template, which contains values to be set when applying the template", + "parameters": [ + { + "type": "string", + "description": "name of the template (template names are unique)", + "name": "templateName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/spec/apply/:anchorArtefactId": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets the spec for a template, which contains values to be set when applying the template (triggers transaction)", + "parameters": [ + { + "type": "string", + "description": "id of the artefact where the template shall be applied. Must be a directory.", + "name": "anchorArtefactId", + "in": "path", + "required": true + }, + { + "description": "the body shall contain a CreateTemplateRequest instance", + "name": "TemplateSpec", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/templates.TemplateSpec" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/templates.Template" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } + }, + "/api/beta/templates/stages": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "gets all available stage templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/config.ApiError" + } + } + } + } } }, "definitions": { @@ -403,6 +745,43 @@ } } }, + "files.CreateFileRequest": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + } + } + }, + "files.Dir": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/files.File" + } + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "subDirs": { + "type": "array", + "items": { + "$ref": "#/definitions/files.Dir" + } + } + } + }, "files.File": { "type": "object", "properties": { @@ -417,7 +796,7 @@ } } }, - "files.Overwrite": { + "overwrites.Overwrite": { "type": "object", "properties": { "originFileId": { @@ -433,6 +812,137 @@ "type": "integer" } } + }, + "templates.CreateTemplateRequest": { + "type": "object", + "properties": { + "template": { + "description": "the toplevel directories name is the name of the template", + "allOf": [ + { + "$ref": "#/definitions/templates.TemplateDir" + } + ] + }, + "templateConfig": { + "$ref": "#/definitions/templates.TemplateConfig" + }, + "templateKind": { + "$ref": "#/definitions/templates.TemplateKind" + } + } + }, + "templates.Template": { + "type": "object", + "properties": { + "children": { + "description": "The children are templates which are contained within the template.", + "type": "array", + "items": { + "$ref": "#/definitions/templates.Template" + } + }, + "kind": { + "$ref": "#/definitions/templates.TemplateKind" + }, + "name": { + "description": "be aware, that each template must have an unique name\nit doesn't matter if they are of different template kind", + "type": "string" + }, + "root": { + "$ref": "#/definitions/files.Dir" + } + } + }, + "templates.TemplateConfig": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "type": "string" + } + }, + "kind": { + "$ref": "#/definitions/templates.TemplateKind" + } + } + }, + "templates.TemplateDir": { + "type": "object", + "properties": { + "directories": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.TemplateDir" + } + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.TemplateFile" + } + }, + "name": { + "type": "string" + } + } + }, + "templates.TemplateFile": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "templateBody": { + "description": "base64 encoded", + "type": "string" + } + } + }, + "templates.TemplateKind": { + "type": "integer", + "enum": [ + 1, + 2, + 4 + ], + "x-enum-varnames": [ + "APPLICATION", + "STAGE", + "DEPLOYMENT" + ] + }, + "templates.TemplateSpec": { + "type": "object", + "properties": { + "template": { + "$ref": "#/definitions/templates.Template" + }, + "valueSpecs": { + "type": "array", + "items": { + "$ref": "#/definitions/templates.ValueSpec" + } + } + } + }, + "templates.ValueSpec": { + "type": "object", + "properties": { + "name": { + "description": "name of the value (name displayed in frontend)", + "type": "string" + }, + "templateName": { + "description": "name of the template which the value belongs to", + "type": "string" + }, + "value": { + "description": "the value assigned", + "type": "string" + } + } } } } \ No newline at end of file diff --git a/momentum-core/docs/swagger.yaml b/momentum-core/docs/swagger.yaml index fd732c5..9e28c5b 100644 --- a/momentum-core/docs/swagger.yaml +++ b/momentum-core/docs/swagger.yaml @@ -47,6 +47,30 @@ definitions: type: type: string type: object + files.CreateFileRequest: + properties: + body: + type: string + name: + type: string + parentId: + type: string + type: object + files.Dir: + properties: + files: + items: + $ref: '#/definitions/files.File' + type: array + id: + type: string + name: + type: string + subDirs: + items: + $ref: '#/definitions/files.Dir' + type: array + type: object files.File: properties: body: @@ -56,7 +80,7 @@ definitions: name: type: string type: object - files.Overwrite: + overwrites.Overwrite: properties: originFileId: type: string @@ -67,6 +91,95 @@ definitions: overwriteFileLine: type: integer type: object + templates.CreateTemplateRequest: + properties: + template: + allOf: + - $ref: '#/definitions/templates.TemplateDir' + description: the toplevel directories name is the name of the template + templateConfig: + $ref: '#/definitions/templates.TemplateConfig' + templateKind: + $ref: '#/definitions/templates.TemplateKind' + type: object + templates.Template: + properties: + children: + description: The children are templates which are contained within the template. + items: + $ref: '#/definitions/templates.Template' + type: array + kind: + $ref: '#/definitions/templates.TemplateKind' + name: + description: |- + be aware, that each template must have an unique name + it doesn't matter if they are of different template kind + type: string + root: + $ref: '#/definitions/files.Dir' + type: object + templates.TemplateConfig: + properties: + children: + items: + type: string + type: array + kind: + $ref: '#/definitions/templates.TemplateKind' + type: object + templates.TemplateDir: + properties: + directories: + items: + $ref: '#/definitions/templates.TemplateDir' + type: array + files: + items: + $ref: '#/definitions/templates.TemplateFile' + type: array + name: + type: string + type: object + templates.TemplateFile: + properties: + name: + type: string + templateBody: + description: base64 encoded + type: string + type: object + templates.TemplateKind: + enum: + - 1 + - 2 + - 4 + type: integer + x-enum-varnames: + - APPLICATION + - STAGE + - DEPLOYMENT + templates.TemplateSpec: + properties: + template: + $ref: '#/definitions/templates.Template' + valueSpecs: + items: + $ref: '#/definitions/templates.ValueSpec' + type: array + type: object + templates.ValueSpec: + properties: + name: + description: name of the value (name displayed in frontend) + type: string + templateName: + description: name of the template which the value belongs to + type: string + value: + description: the value assigned + type: string + type: object host: localhost:8080 info: contact: {} @@ -168,6 +281,13 @@ paths: post: consumes: - application/json + parameters: + - description: the body shall contain a File instance + in: body + name: CreateFileRequest + required: true + schema: + $ref: '#/definitions/files.CreateFileRequest' produces: - application/json responses: @@ -187,7 +307,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/config.ApiError' - summary: adds a new file to a given parent + summary: adds a new file to a given parent (triggers transaction) tags: - files /api/beta/file/{id}: @@ -220,6 +340,43 @@ paths: summary: gets the content of a file tags: - files + put: + consumes: + - application/json + parameters: + - description: file id + in: path + name: id + required: true + type: string + - description: the body shall contain a File instance + in: body + name: File + required: true + schema: + $ref: '#/definitions/files.File' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/files.File' + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: updates the given file (triggers transaction) + tags: + - files /api/beta/file/{id}/line/{lineNumber}/overwritten-by: get: parameters: @@ -240,7 +397,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/files.Overwrite' + $ref: '#/definitions/overwrites.Overwrite' type: array "400": description: Bad Request @@ -254,7 +411,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/config.ApiError' - summary: gets a list of properties which overwrite the given line. + summary: gets a list of overwrites which overwrite the given line. tags: - files /api/beta/stages: @@ -287,6 +444,188 @@ paths: summary: gets a list of all stages within an application or stage by id. tags: - artefacts + /api/beta/templates: + post: + consumes: + - application/json + parameters: + - description: the body shall contain a CreateTemplateRequest instance + in: body + name: CreateTemplateRequest + required: true + schema: + $ref: '#/definitions/templates.CreateTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/templates.Template' + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: adds a new template (triggers transaction) + tags: + - templates + /api/beta/templates/applications: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: gets all available application templates + tags: + - templates + /api/beta/templates/deployments: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: gets all available deployment templates + tags: + - templates + /api/beta/templates/spec/:templateName: + get: + parameters: + - description: name of the template (template names are unique) + in: path + name: templateName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/templates.Template' + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: gets the spec for a template, which contains values to be set when + applying the template + tags: + - templates + /api/beta/templates/spec/apply/:anchorArtefactId: + post: + consumes: + - application/json + parameters: + - description: id of the artefact where the template shall be applied. Must + be a directory. + in: path + name: anchorArtefactId + required: true + type: string + - description: the body shall contain a CreateTemplateRequest instance + in: body + name: TemplateSpec + required: true + schema: + $ref: '#/definitions/templates.TemplateSpec' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/templates.Template' + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: gets the spec for a template, which contains values to be set when + applying the template (triggers transaction) + tags: + - templates + /api/beta/templates/stages: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/config.ApiError' + "404": + description: Not Found + schema: + $ref: '#/definitions/config.ApiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/config.ApiError' + summary: gets all available stage templates + tags: + - templates schemes: - http swagger: "2.0" diff --git a/momentum-core/files/files.go b/momentum-core/files/files.go index e9eb2cc..9d9dfed 100644 --- a/momentum-core/files/files.go +++ b/momentum-core/files/files.go @@ -11,13 +11,14 @@ func fileToBase64(path string) (string, error) { if err != nil { return "", err } + defer f.Close() fileAsString := utils.FileAsString(f) return base64.RawStdEncoding.EncodeToString([]byte(fileAsString)), nil } -func fileToRaw(base64Encoded string) (string, error) { +func FileToRaw(base64Encoded string) (string, error) { bytes, err := base64.RawStdEncoding.DecodeString(base64Encoded) if err != nil { diff --git a/momentum-core/files/routes.go b/momentum-core/files/routes.go index 5d9ed5d..9eed50f 100644 --- a/momentum-core/files/routes.go +++ b/momentum-core/files/routes.go @@ -3,14 +3,12 @@ package files import ( "errors" "momentum-core/artefacts" - "momentum-core/backtracking" "momentum-core/config" + "momentum-core/overwrites" "momentum-core/utils" - "momentum-core/yaml" "net/http" "path/filepath" "strconv" - "strings" gittransaction "github.com/Joel-Haeberli/git-transaction" "github.com/gin-gonic/gin" @@ -19,6 +17,7 @@ import ( func RegisterFileRoutes(engine *gin.Engine) { engine.GET(config.API_FILE_BY_ID, GetFile) engine.POST(config.API_FILE_ADD, AddFile) + engine.PUT(config.API_FILE_UPDATE, UpdateFile) engine.GET(config.API_FILE_LINE_OVERWRITTENBY, GetOverwrittenBy) } @@ -57,7 +56,8 @@ func GetFile(c *gin.Context) { // @Tags files // @Accept json // @Produce json -// @Body CreateFileRequest +// @Body json +// @Param CreateFileRequest body CreateFileRequest true "the body shall contain a File instance" // @Success 200 {object} File // @Failure 400 {object} config.ApiError // @Failure 404 {object} config.ApiError @@ -91,7 +91,7 @@ func AddFile(c *gin.Context) { return } - fileContentDecoded, err := fileToRaw(createFileReq.Body) + fileContentDecoded, err := FileToRaw(createFileReq.Body) if err != nil { c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) config.LOGGER.LogError(err.Error(), err, traceId) @@ -157,7 +157,9 @@ func AddFile(c *gin.Context) { // @Tags files // @Accept json // @Produce json -// @Body File +// @Body json +// @Param id path string true "file id" +// @Param File body File true "the body shall contain a File instance" // @Success 200 {object} File // @Failure 400 {object} config.ApiError // @Failure 404 {object} config.ApiError @@ -175,7 +177,7 @@ func UpdateFile(c *gin.Context) { return } - decodedBody, err := fileToRaw(requestedFile.Body) + decodedBody, err := FileToRaw(requestedFile.Body) if err != nil { c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) config.LOGGER.LogError(err.Error(), err, traceId) @@ -238,7 +240,7 @@ func UpdateFile(c *gin.Context) { // @Produce json // @Param id path string true "file id" // @Param lineNumber path int true "line number in file" -// @Success 200 {array} Overwrite +// @Success 200 {array} overwrites.Overwrite // @Failure 400 {object} config.ApiError // @Failure 404 {object} config.ApiError // @Failure 500 {object} config.ApiError @@ -262,52 +264,18 @@ func GetOverwrittenBy(c *gin.Context) { return } - fileNode, err := yaml.ParseFile(artefacts.FullPath(overwritable)) - if err != nil { - c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) - config.LOGGER.LogError(err.Error(), err, traceId) - return - } - - lineNode := yaml.FindNodeByLine(fileNode, overwritableLine) - if lineNode == nil { - err := errors.New("could not find line " + strconv.Itoa(overwritableLine) + " in file " + artefacts.FullPath(overwritable)) - c.JSON(http.StatusNotFound, config.NewApiError(err, http.StatusNotFound, c, traceId)) - config.LOGGER.LogError(err.Error(), err, traceId) - return - } - - overwritingFiles := make([]*artefacts.Artefact, 0) - for _, advice := range artefacts.ActiveOverwriteAdvices { - overwritingFiles = append(overwritingFiles, advice(artefacts.FullPath(overwritable))...) - } - - if len(overwritingFiles) > 0 { - - predicate := yaml.ToMatchableSearchTerm(lineNode.FullPath()) - predicate = strings.Join(strings.Split(predicate, ".")[1:], ".") // remove filename prefix + ovrwrts := make([]*overwrites.Overwrite, 0) + for _, provider := range overwrites.ActiveOverwriteProviders { - overwrites := make([]*Overwrite, 0) - for _, overwriting := range overwritingFiles { - - backtracker := backtracking.NewPropertyBacktracker(predicate, artefacts.FullPath(overwriting), backtracking.NewYamlPropertyParser()) - var result []*backtracking.Match[string, yaml.ViewNode] = backtracker.RunBacktrack() - - for _, match := range result { - - overwrite := new(Overwrite) - overwrite.OriginFileId = overwritableId - overwrite.OriginFileLine = overwritableLine - overwrite.OverwriteFileLine = match.MatchNode.Pointer.YamlNode.Line - overwrite.OverwriteFileId = overwriting.Id - - overwrites = append(overwrites, overwrite) - } + o, err := provider(overwritable, overwritableLine) + if err != nil { + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return } - c.JSON(http.StatusOK, overwrites) - return + ovrwrts = append(ovrwrts, o...) } - c.JSON(http.StatusOK, make([]*Overwrite, 0)) + c.JSON(http.StatusOK, ovrwrts) } diff --git a/momentum-core/main.go b/momentum-core/main.go index b45e1d6..05b89a7 100644 --- a/momentum-core/main.go +++ b/momentum-core/main.go @@ -5,6 +5,7 @@ import ( "momentum-core/artefacts" "momentum-core/config" "momentum-core/files" + "momentum-core/templates" "github.com/gin-gonic/gin" @@ -39,10 +40,13 @@ func main() { // gitClient := clients.NewGitClient(config) // kustomizeClient := clients.NewKustomizationValidationClient(config) + //gin.SetMode(gin.ReleaseMode) + server := gin.Default() files.RegisterFileRoutes(server) artefacts.RegisterArtefactRoutes(server) + templates.RegisterTemplateRoutes(server) server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/momentum-core/overwrites/encoder.go b/momentum-core/overwrites/encoder.go new file mode 100644 index 0000000..0edf9b2 --- /dev/null +++ b/momentum-core/overwrites/encoder.go @@ -0,0 +1,41 @@ +package overwrites + +import ( + "momentum-core/utils" + "os" + + "gopkg.in/yaml.v3" +) + +func rulesFromFile(path string) (*OverwriteConfig, error) { + + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + conf := new(OverwriteConfig) + err = yaml.NewDecoder(f).Decode(conf) + if err != nil { + return nil, err + } + + return conf, nil +} + +func rulesToFile(path string, rules *OverwriteConfig) (string, error) { + + f, err := utils.FileOpen(path, os.O_WRONLY) + if err != nil { + return "", err + } + defer f.Close() + + err = yaml.NewEncoder(f).Encode(rules) + if err != nil { + return "", err + } + + return path, nil +} diff --git a/momentum-core/overwrites/model.go b/momentum-core/overwrites/model.go new file mode 100644 index 0000000..bfe12af --- /dev/null +++ b/momentum-core/overwrites/model.go @@ -0,0 +1,46 @@ +package overwrites + +// the direction of the rule defines in which direction the destination is pointing. +type OverwriteRuleDirection int + +// the strategy defines on which properties, the overwrites are based. +type OverwriteStrategy int + +const ( + UP OverwriteRuleDirection = 1 << iota + DOWN + INTERN // this means the overwrite rule will overwrite stuff inside the same level +) + +const ( + PATH OverwriteStrategy = 1 << iota // PATH will overwrite files with the exact paths given + NAME // NAME will overwrite files with the same name in the given direction +) + +type Overwrite struct { + OriginFileId string `json:"originFileId"` + OriginFileLine int `json:"originFileLine"` + OverwriteFileId string `json:"overwriteFileId"` + OverwriteFileLine int `json:"overwriteFileLine"` +} + +type OverwriteConfig struct { + // list of all rules applying to containing location. + // order of the elements is important because it also defines + // the priority of the rule within the rule set. This means + // that the first rule in the list is the most important and + // the last rule in the list is the least important. + Rules []*OverwriteRule `yaml:"rules" json:"rules"` +} + +// An overwrite rule is defined relative to the containing artefact. +type OverwriteRule struct { + // in which way the rule shall be applied / point inside the structure + Direction OverwriteRuleDirection `yaml:"direction" json:"direction"` + // the strategy of the rule + Strategy OverwriteStrategy `yaml:"strategy" json:"strategy"` + // the path of the file which shall overwrite + Origin string `yaml:"origin,omitempty" json:"origin,omitempty"` + // the path of the file which shall be overwritten. The destination path is relative to the origin path + Destination string `yaml:"destination,omitempty" json:"destination,omitempty"` +} diff --git a/momentum-core/overwrites/overwrites.go b/momentum-core/overwrites/overwrites.go new file mode 100644 index 0000000..f963780 --- /dev/null +++ b/momentum-core/overwrites/overwrites.go @@ -0,0 +1,101 @@ +package overwrites + +import ( + "errors" + "momentum-core/artefacts" + "momentum-core/backtracking" + "momentum-core/yaml" + "strconv" + "strings" +) + +type OverwriteProvider func(origin *artefacts.Artefact, originLineNumber int) ([]*Overwrite, error) + +// extend this list to add more overwriting rules +var ActiveOverwriteProviders []OverwriteProvider = []OverwriteProvider{defaultOverwriteBehaviour, ruleEngineOverwriteBehaviour} + +func OverwriteConfigByArtefact(artefactId string) *OverwriteConfig { + + return nil +} + +func defaultOverwriteBehaviour(origin *artefacts.Artefact, originLineNumber int) ([]*Overwrite, error) { + + fileNode, err := yaml.ParseFile(artefacts.FullPath(origin)) + if err != nil { + return make([]*Overwrite, 0), err + } + + lineNode := yaml.FindNodeByLine(fileNode, originLineNumber) + if lineNode == nil { + err := errors.New("could not find line " + strconv.Itoa(originLineNumber) + " in file " + artefacts.FullPath(origin)) + return make([]*Overwrite, 0), err + } + + overwritingFiles := overwritesByFilenamePriorityAsc(artefacts.FullPath(origin)) + + if len(overwritingFiles) > 0 { + + predicate := yaml.ToMatchableSearchTerm(lineNode.FullPath()) + predicate = strings.Join(strings.Split(predicate, ".")[1:], ".") // remove filename prefix + + overwrites := make([]*Overwrite, 0) + for _, overwriting := range overwritingFiles { + + backtracker := backtracking.NewPropertyBacktracker(predicate, artefacts.FullPath(overwriting), backtracking.NewYamlPropertyParser()) + var result []*backtracking.Match[string, yaml.ViewNode] = backtracker.RunBacktrack() + + for _, match := range result { + + overwrite := new(Overwrite) + overwrite.OriginFileId = origin.Id + overwrite.OriginFileLine = originLineNumber + overwrite.OverwriteFileLine = match.MatchNode.Pointer.YamlNode.Line + overwrite.OverwriteFileId = overwriting.Id + + overwrites = append(overwrites, overwrite) + } + } + + return overwrites, nil + } + + return make([]*Overwrite, 0), nil +} + +// gets all files which are higher up in the structure with the same name as the given file path. +// first item in result is most important +func overwritesByFilenamePriorityAsc(path string) []*artefacts.Artefact { + + overwritable := artefacts.FindArtefactByPath(path) + if overwritable != nil { + + overwritesOrderedByPriorityAsc := make([]*artefacts.Artefact, 0) + current := overwritable + for current != nil { + + for _, child := range current.Content { + + if strings.EqualFold(overwritable.Name, child.Name) && !strings.EqualFold(overwritable.Id, child.Id) { + overwritesOrderedByPriorityAsc = append(overwritesOrderedByPriorityAsc, child) + } + } + + current = current.Parent + } + + return overwritesOrderedByPriorityAsc + } + + return make([]*artefacts.Artefact, 0) +} + +func ruleEngineOverwriteBehaviour(origin *artefacts.Artefact, originLineNumber int) ([]*Overwrite, error) { + + return make([]*Overwrite, 0), nil +} + +func rulesByFilename(path string) []*artefacts.Artefact { + + return make([]*artefacts.Artefact, 0) +} diff --git a/momentum-core/overwrites/rule-engine.go b/momentum-core/overwrites/rule-engine.go new file mode 100644 index 0000000..e6d4e4b --- /dev/null +++ b/momentum-core/overwrites/rule-engine.go @@ -0,0 +1,21 @@ +package overwrites + +func ApplyRule(rule *OverwriteRule) bool { + + // TODO implement applying rules + return false +} + +// rules are applied in reversed order, which leads to the behavior +// that rules occuring earlier in the list have higher priority than +// items later in the list. +func ApplyRules(rules []*OverwriteRule) bool { + + numberOfRules := len(rules) + for i := range rules { + if !ApplyRule(rules[numberOfRules-1-i]) { + return false + } + } + return true +} diff --git a/momentum-core/templates/encoder.go b/momentum-core/templates/encoder.go new file mode 100644 index 0000000..6072b27 --- /dev/null +++ b/momentum-core/templates/encoder.go @@ -0,0 +1,41 @@ +package templates + +import ( + "momentum-core/utils" + "os" + + "gopkg.in/yaml.v3" +) + +func templateConfigFromFile(path string) (*TemplateConfig, error) { + + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + conf := new(TemplateConfig) + err = yaml.NewDecoder(f).Decode(conf) + if err != nil { + return nil, err + } + + return conf, nil +} + +func templateConfigToFile(path string, rules *TemplateConfig) (string, error) { + + f, err := utils.FileOpen(path, os.O_CREATE|os.O_WRONLY) + if err != nil { + return "", err + } + defer f.Close() + + err = yaml.NewEncoder(f).Encode(rules) + if err != nil { + return "", err + } + + return path, nil +} diff --git a/momentum-core/templates/example-template.json b/momentum-core/templates/example-template.json new file mode 100644 index 0000000..b5c353b --- /dev/null +++ b/momentum-core/templates/example-template.json @@ -0,0 +1,36 @@ +{ + "template": { + "directories": [ + { + "directories": [], + "files": [ + { + "name": "values.yaml", + "templateBody": "aToKICBtOiBhIGR1bW15" + }, + { + "name": "secret.yaml", + "templateBody": "aToKICBtOiBhIGR1bW15" + } + ], + "name": "_base" + } + ], + "files": [ + { + "name": "kustomization.yaml", + "templateBody": "aToKICBtOiBhIGR1bW15" + }, + { + "name": "ns.yaml", + "templateBody": "aToKICBtOiBhIGR1bW15" + } + ], + "name": "test-app-template" + }, + "templateConfig": { + "children": [], + "kind": 1 + }, + "templateKind": 1 + } \ No newline at end of file diff --git a/momentum-core/templates/model.go b/momentum-core/templates/model.go index 7b166bb..54d4df5 100644 --- a/momentum-core/templates/model.go +++ b/momentum-core/templates/model.go @@ -1,8 +1,9 @@ package templates -import "momentum-core/files" - -type TemplateStructureType int +import ( + "momentum-core/files" + "momentum-core/overwrites" +) type TemplateKind int @@ -12,31 +13,47 @@ const ( DEPLOYMENT ) -const ( - DIR TemplateStructureType = 1 << iota - FILE -) +type TemplateDir struct { + Name string `json:"name"` + Directories []*TemplateDir `json:"directories"` + Files []*TemplateFile `json:"files"` +} + +type TemplateFile struct { + Name string `json:"name"` + TemplateBody string `json:"templateBody"` // base64 encoded +} + +type TemplateConfig struct { + Kind TemplateKind `json:"kind" yaml:"kind"` + Children []string `json:"children" yaml:"children"` +} type CreateTemplateRequest struct { TemplateKind TemplateKind `json:"templateKind"` - Template *files.Dir `json:"template"` + // the toplevel directories name is the name of the template + Template *TemplateDir `json:"template"` + TemplateConfig *TemplateConfig `json:"templateConfig"` + OverwriteConfig *overwrites.OverwriteConfig `json:"-"` // `json:"overwriteConfig"` // TODO: implemented later } type Template struct { - TemplateKind TemplateKind `json:"templateKind"` - Template *files.Dir `json:"template"` + // be aware, that each template must have an unique name + // it doesn't matter if they are of different template kind + Name string `json:"name"` + Kind TemplateKind `json:"kind"` + Root *files.Dir `json:"root"` + // The children are templates which are contained within the template. + Children []*Template `json:"children"` } -type TemplateStore struct { - Templates []*Template `json:"templates"` +type TemplateSpec struct { + Template *Template `json:"template"` + ValueSpecs []*ValueSpec `json:"valueSpecs"` } -// next steps: -// 1. define templates -// 2. define implement overwrite detection by file -// 3. implement backtracking with detected files for line matching endpoints - -// this shall define which files overwrite each other. -// shall support wildcards and filenames. -type OverwriteConfiguration struct { +type ValueSpec struct { + TemplateName string `json:"templateName"` // name of the template which the value belongs to + Name string `json:"name"` // name of the value (name displayed in frontend) + Value string `json:"value"` // the value assigned } diff --git a/momentum-core/templates/routes.go b/momentum-core/templates/routes.go index dac8432..813cfaf 100644 --- a/momentum-core/templates/routes.go +++ b/momentum-core/templates/routes.go @@ -1 +1,208 @@ package templates + +import ( + "errors" + "momentum-core/config" + "net/http" + + gittransaction "github.com/Joel-Haeberli/git-transaction" + "github.com/gin-gonic/gin" +) + +func RegisterTemplateRoutes(engine *gin.Engine) { + engine.GET(config.API_TEMPLATES_APPLICATIONS, GetApplicationTemplates) + engine.GET(config.API_TEMPLATES_STAGES, GetStageTemplates) + engine.GET(config.API_TEMPLATES_DEPLOYMENTS, GetDeploymentTemplates) + engine.POST(config.API_TEMPLATES_ADD, AddTemplate) + engine.GET(config.API_TEMPLATE_GET_SPEC, GetTemplateSpec) + engine.POST(config.API_TEMPLATE_APPLY, ApplyDeploymentTemplate) +} + +// GetApplicationTemplates godoc +// +// @Summary gets all available application templates +// @Tags templates +// @Produce json +// @Success 200 {array} string +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates/applications [get] +func GetApplicationTemplates(c *gin.Context) { + + result := TemplateNames(config.ApplicationTemplatesPath(config.GLOBAL)) + c.JSON(http.StatusOK, result) +} + +// GetStageTemplates godoc +// +// @Summary gets all available stage templates +// @Tags templates +// @Produce json +// @Success 200 {array} string +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates/stages [get] +func GetStageTemplates(c *gin.Context) { + + result := TemplateNames(config.StageTemplatesPath(config.GLOBAL)) + c.JSON(http.StatusOK, result) +} + +// GetDeploymentTemplates godoc +// +// @Summary gets all available deployment templates +// @Tags templates +// @Produce json +// @Success 200 {array} string +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates/deployments [get] +func GetDeploymentTemplates(c *gin.Context) { + + result := TemplateNames(config.DeploymentTemplatesPath(config.GLOBAL)) + c.JSON(http.StatusOK, result) +} + +// AddTemplate godoc +// +// @Summary adds a new template (triggers transaction) +// @Tags templates +// @Accept json +// @Produce json +// @Body json +// @Param CreateTemplateRequest body CreateTemplateRequest true "the body shall contain a CreateTemplateRequest instance" +// @Success 200 {object} Template +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates [post] +func AddTemplate(c *gin.Context) { + + traceId := config.LOGGER.TraceId() + + request := new(CreateTemplateRequest) + err := c.BindJSON(request) + if err != nil { + c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + if TemplateExists(request.Template.Name) { + err = errors.New("template with this name already exists") + c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + if len(request.Template.Directories) == 0 && len(request.Template.Files) == 0 { + err = errors.New("expecting non empty template") + c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + validConfig, err := validTemplateConfig(request.TemplateConfig, request.TemplateKind) + if !validConfig { + c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + ctx, transaction, err := gittransaction.New(config.TRANSACTION_MODE, config.GLOBAL.RepoDir(), config.GLOBAL.TransactionToken()) + if err != nil { + transaction.Rollback(ctx) + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + temp, err := CreateTemplate(request) + if err != nil { + if err != nil { + c.JSON(http.StatusBadRequest, config.NewApiError(err, http.StatusBadRequest, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + } + + err = transaction.Write(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + err = transaction.Commit(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + c.JSON(http.StatusOK, temp) +} + +// GetTemplateSpec godoc +// +// @Summary gets the spec for a template, which contains values to be set when applying the template +// @Tags templates +// @Produce json +// @Param templateName path string true "name of the template (template names are unique)" +// @Success 200 {object} Template +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates/spec/:templateName [get] +func GetTemplateSpec(c *gin.Context) { + + c.JSON(http.StatusNoContent, config.NewApiError(errors.New("not implemented yet"), http.StatusNoContent, c, config.LOGGER.TraceId())) +} + +// ApplyDeploymentTemplate godoc +// +// @Summary gets the spec for a template, which contains values to be set when applying the template (triggers transaction) +// @Tags templates +// @Accept json +// @Produce json +// @Body json +// @Param anchorArtefactId path string true "id of the artefact where the template shall be applied. Must be a directory." +// @Param TemplateSpec body TemplateSpec true "the body shall contain a CreateTemplateRequest instance" +// @Success 200 {object} Template +// @Failure 400 {object} config.ApiError +// @Failure 404 {object} config.ApiError +// @Failure 500 {object} config.ApiError +// @Router /api/beta/templates/spec/apply/:anchorArtefactId [post] +func ApplyDeploymentTemplate(c *gin.Context) { + + traceId := config.LOGGER.TraceId() + + ctx, transaction, err := gittransaction.New(config.TRANSACTION_MODE, config.GLOBAL.RepoDir(), config.GLOBAL.TransactionToken()) + if err != nil { + transaction.Rollback(ctx) + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + // TODO APPLY TEMPLATE HERE + + err = transaction.Write(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + err = transaction.Commit(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, config.NewApiError(err, http.StatusInternalServerError, c, traceId)) + config.LOGGER.LogError(err.Error(), err, traceId) + return + } + + c.JSON(http.StatusNoContent, config.NewApiError(errors.New("not implemented yet"), http.StatusNoContent, c, config.LOGGER.TraceId())) +} diff --git a/momentum-core/templates/template-config-parser.go b/momentum-core/templates/template-config-parser.go deleted file mode 100644 index dac8432..0000000 --- a/momentum-core/templates/template-config-parser.go +++ /dev/null @@ -1 +0,0 @@ -package templates diff --git a/momentum-core/templates/templates.go b/momentum-core/templates/templates.go new file mode 100644 index 0000000..164e7ce --- /dev/null +++ b/momentum-core/templates/templates.go @@ -0,0 +1,198 @@ +package templates + +import ( + "errors" + "io/fs" + "momentum-core/config" + "momentum-core/files" + "momentum-core/utils" + "os" + "path/filepath" + "strings" +) + +const TEMPLATE_CONFIG_FILENAME = "momentum-template-config.yaml" + +func LoadSpec(templateName string, templateKind TemplateKind) []*TemplateSpec { + + return make([]*TemplateSpec, 0) +} + +func LoadTemplate(templateName string) (*Template, error) { + + if !TemplateExists(templateName) { + return nil, errors.New("no template name '" + templateName + "'") + } + + //files. + + template := new(Template) + //template.Kind = + + return template, nil +} + +func CreateTemplate(templateRequest *CreateTemplateRequest) (*Template, error) { + + templateAnchorPath := filepath.Join(templatePathForKind(templateRequest.TemplateKind)) + + err := createTemplateDir(templateAnchorPath, templateRequest.Template) + if err != nil { + return nil, err + } + + _, err = templateConfigToFile(filepath.Join(templateAnchorPath, templateRequest.Template.Name, TEMPLATE_CONFIG_FILENAME), templateRequest.TemplateConfig) + if err != nil { + return nil, err + } + + return LoadTemplate(templateRequest.Template.Name) +} + +func createTemplateDir(anchorPath string, templateDir *TemplateDir) error { + + templateAnchorPath := filepath.Join(anchorPath, templateDir.Name) + err := utils.DirCreate(templateAnchorPath) + if err != nil { + return err + } + + for _, dir := range templateDir.Directories { + err := createTemplateDir(templateAnchorPath, dir) + if err != nil { + return err + } + } + + for _, file := range templateDir.Files { + err := createTemplateFile(templateAnchorPath, file) + if err != nil { + return err + } + } + + return nil +} + +func createTemplateFile(anchorPath string, templateFile *TemplateFile) error { + + if !strings.HasSuffix(templateFile.Name, ".yaml") && !strings.HasSuffix(templateFile.Name, ".yml") { + return errors.New("only yaml files supported at the moment (you sent " + templateFile.Name + ")") + } + + path := filepath.Join(anchorPath, templateFile.Name) + body, err := files.FileToRaw(templateFile.TemplateBody) + if err != nil { + return err + } + + success := utils.FileWrite(path, body) + if !success { + return errors.New("unable to write file '" + path + "'") + } + return nil +} + +func templatePathForKind(kind TemplateKind) string { + + switch kind { + case APPLICATION: + return config.ApplicationTemplatesPath(config.GLOBAL) + case STAGE: + return config.StageTemplatesPath(config.GLOBAL) + case DEPLOYMENT: + return config.DeploymentTemplatesPath(config.GLOBAL) + default: + return "" + } +} + +func TemplateNames(path string) []string { + + entrs, err := entries(path) + if err != nil { + return make([]string, 0) + } + + names := make([]string, 0) + for _, entry := range entrs { + names = append(names, entry.Name()) + } + + return names +} + +func TemplateExists(name string) bool { + + return applicationExists(name) || stageExists(name) || deploymentExists(name) +} + +// validates a given config. A config is valid if all contained children are present. +func validTemplateConfig(templateConfig *TemplateConfig, templateKind TemplateKind) (bool, error) { + + if templateKind != templateConfig.Kind { + return false, errors.New("template config must have same kind as request") + } + + for _, temp := range templateConfig.Children { + + if !TemplateExists(temp) { + return false, errors.New("template with name '" + temp + "' does not exist") + } + } + + return true, nil +} + +func applicationExists(name string) bool { + + appTemplates := config.ApplicationTemplatesPath(config.GLOBAL) + entries, err := entries(appTemplates) + if err != nil { + return false + } + return directoryContains(name, entries) +} + +func stageExists(name string) bool { + + stageTemplates := config.ApplicationTemplatesPath(config.GLOBAL) + entries, err := entries(stageTemplates) + if err != nil { + return false + } + return directoryContains(name, entries) +} + +func deploymentExists(name string) bool { + + deploymentTemplates := config.ApplicationTemplatesPath(config.GLOBAL) + entries, err := entries(deploymentTemplates) + if err != nil { + return false + } + return directoryContains(name, entries) +} + +func entries(dirPath string) ([]fs.DirEntry, error) { + + dir, err := utils.FileOpen(dirPath, int(os.ModeDir.Perm())) + if err != nil { + return nil, err + } + defer dir.Close() + + return dir.ReadDir(-1) // reads all entries +} + +func directoryContains(name string, entries []fs.DirEntry) bool { + + for _, entry := range entries { + + if strings.EqualFold(entry.Name(), name) { + return true + } + } + + return false +} diff --git a/momentum-core/utils/gin-body-extractor.go b/momentum-core/utils/gin-body-extractor.go deleted file mode 100644 index e713651..0000000 --- a/momentum-core/utils/gin-body-extractor.go +++ /dev/null @@ -1,13 +0,0 @@ -package utils - -import "github.com/gin-gonic/gin" - -func Extract[T any](c *gin.Context) (*T, error) { - - t := new(T) - err := c.BindJSON(t) - if err != nil { - return nil, err - } - return t, nil -} diff --git a/momentum-core/yaml/tree.go b/momentum-core/yaml/tree.go index 5d6f7f7..a463a11 100644 --- a/momentum-core/yaml/tree.go +++ b/momentum-core/yaml/tree.go @@ -3,6 +3,7 @@ package yaml import ( "errors" "fmt" + "momentum-core/config" "momentum-core/utils" "strings" ) @@ -56,7 +57,7 @@ func NewNode(kind NodeKind, path string, value string, parent *ViewNode, childre n.IsWrapping = true } - id, err := utils.GenerateId(n.FullPath()) + id, err := utils.GenerateId(config.IdGenerationPath(n.FullPath())) if err != nil { fmt.Println("generating id failed:", err.Error()) }