diff --git a/api/http/handler/gitops/git_repo_file_preview.go b/api/http/handler/gitops/git_repo_file_preview.go new file mode 100644 index 000000000..c2ec131c7 --- /dev/null +++ b/api/http/handler/gitops/git_repo_file_preview.go @@ -0,0 +1,87 @@ +package gitops + +import ( + "errors" + "fmt" + "net/http" + + "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + gittypes "github.com/portainer/portainer/api/git/types" +) + +type fileResponse struct { + FileContent string +} + +type repositoryFilePreviewPayload struct { + Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"` + Reference string `json:"reference" example:"refs/heads/master"` + Username string `json:"username" example:"myGitUsername"` + Password string `json:"password" example:"myGitPassword"` + // Path to file whose content will be read + TargetFile string `json:"targetFile" example:"docker-compose.yml"` +} + +func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Repository) || !govalidator.IsURL(payload.Repository) { + return errors.New("Invalid repository URL. Must correspond to a valid URL format") + } + + if govalidator.IsNull(payload.Reference) { + payload.Reference = "refs/heads/main" + } + + if govalidator.IsNull(payload.TargetFile) { + return errors.New("Invalid target filename.") + } + + return nil +} + +// @id GitOperationRepoFilePreview +// @summary preview the content of target file in the git repository +// @description Retrieve the compose file content based on git repository configuration +// @description **Access policy**: authenticated +// @tags gitops +// @security ApiKeyAuth +// @security jwt +// @produce json +// @param body body repositoryFilePreviewPayload true "Template details" +// @success 200 {object} fileResponse "Success" +// @failure 400 "Invalid request" +// @failure 500 "Server error" +// @router /gitops/repo/file/preview [post] +func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload repositoryFilePreviewPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return httperror.BadRequest("Invalid request payload", err) + } + + projectPath, err := handler.fileService.GetTemporaryPath() + if err != nil { + return httperror.InternalServerError("Unable to create temporary folder", err) + } + + err = handler.gitService.CloneRepository(projectPath, payload.Repository, payload.Reference, payload.Username, payload.Password) + if err != nil { + if err == gittypes.ErrAuthenticationFailure { + return httperror.BadRequest("Invalid git credential", err) + } + + newErr := fmt.Errorf("unable to clone git repository: %w", err) + return httperror.InternalServerError(newErr.Error(), newErr) + } + + defer handler.fileService.RemoveDirectory(projectPath) + + fileContent, err := handler.fileService.GetFileContent(projectPath, payload.TargetFile) + if err != nil { + return httperror.InternalServerError("Unable to retrieve custom template file from disk", err) + } + + return response.JSON(w, &fileResponse{FileContent: string(fileContent)}) +} diff --git a/api/http/handler/gitops/handler.go b/api/http/handler/gitops/handler.go new file mode 100644 index 000000000..7b6693201 --- /dev/null +++ b/api/http/handler/gitops/handler.go @@ -0,0 +1,33 @@ +package gitops + +import ( + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/http/security" +) + +// Handler is the HTTP handler used to handle git repo operation +type Handler struct { + *mux.Router + dataStore dataservices.DataStore + gitService portainer.GitService + fileService portainer.FileService +} + +func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gitService portainer.GitService, fileService portainer.FileService) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + dataStore: dataStore, + gitService: gitService, + fileService: fileService, + } + + h.Handle("/gitops/repo/file/preview", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.gitOperationRepoFilePreview))).Methods(http.MethodPost) + + return h +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 89d43fb90..05ee02bcf 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -18,6 +18,7 @@ import ( "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/gitops" "github.com/portainer/portainer/api/http/handler/helm" "github.com/portainer/portainer/api/http/handler/hostmanagement/fdo" "github.com/portainer/portainer/api/http/handler/hostmanagement/openamt" @@ -58,6 +59,7 @@ type Handler struct { EndpointHandler *endpoints.Handler EndpointHelmHandler *helm.Handler EndpointProxyHandler *endpointproxy.Handler + GitOperationHandler *gitops.Handler HelmTemplatesHandler *helm.Handler KubernetesHandler *kubernetes.Handler FileHandler *file.Handler @@ -123,6 +125,8 @@ type Handler struct { // @tag.description Manage Docker environments(endpoints) // @tag.name endpoint_groups // @tag.description Manage environment(endpoint) groups +// @tag.name gitops +// @tag.description Operate git repository // @tag.name kubernetes // @tag.description Manage Kubernetes cluster // @tag.name motd @@ -207,6 +211,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } + case strings.HasPrefix(r.URL.Path, "/api/gitops"): + http.StripPrefix("/api", h.GitOperationHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/ldap"): http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/motd"): diff --git a/api/http/server.go b/api/http/server.go index a0c64d724..af91d92d7 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -30,6 +30,7 @@ import ( "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/gitops" "github.com/portainer/portainer/api/http/handler/helm" "github.com/portainer/portainer/api/http/handler/hostmanagement/fdo" "github.com/portainer/portainer/api/http/handler/hostmanagement/openamt" @@ -191,6 +192,8 @@ func (server *Server) Start() error { var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService) + var gitOperationHandler = gitops.NewHandler(requestBouncer, server.DataStore, server.GitService, server.FileService) + var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager) var ldapHandler = ldap.NewHandler(requestBouncer) @@ -289,6 +292,7 @@ func (server *Server) Start() error { EndpointHelmHandler: endpointHelmHandler, EndpointEdgeHandler: endpointEdgeHandler, EndpointProxyHandler: endpointProxyHandler, + GitOperationHandler: gitOperationHandler, FileHandler: fileHandler, LDAPHandler: ldapHandler, HelmTemplatesHandler: helmTemplatesHandler,