diff --git a/cyclops-ctrl/internal/controller/templates.go b/cyclops-ctrl/internal/controller/templates.go index f867a81a..69c828ed 100644 --- a/cyclops-ctrl/internal/controller/templates.go +++ b/cyclops-ctrl/internal/controller/templates.go @@ -207,3 +207,22 @@ func (c *Templates) DeleteTemplatesStore(ctx *gin.Context) { ctx.Status(http.StatusOK) } + +func (c *Templates) GetTemplateRevisions(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", "*") + + repo := ctx.Query("repo") + + if repo == "" { + ctx.JSON(http.StatusOK, []string{}) + return + } + + revisions, err := c.templatesRepo.GetTemplateRevisions(repo) + if err != nil { + ctx.JSON(http.StatusBadRequest, dto.NewError("Error loading template", err.Error())) + return + } + + ctx.JSON(http.StatusOK, revisions) +} diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go index 72c0c200..512fdeac 100644 --- a/cyclops-ctrl/internal/handler/handler.go +++ b/cyclops-ctrl/internal/handler/handler.go @@ -79,6 +79,8 @@ func (h *Handler) Start() error { h.router.GET("/templates", templatesController.GetTemplate) h.router.GET("/templates/initial", templatesController.GetTemplateInitialValues) + h.router.GET("/templates/revisions", templatesController.GetTemplateRevisions) + // templates store h.router.GET("/templates/store", templatesController.ListTemplatesStore) h.router.PUT("/templates/store", templatesController.CreateTemplatesStore) diff --git a/cyclops-ctrl/mocks/ITemplateRepo.go b/cyclops-ctrl/mocks/ITemplateRepo.go index 3e8a5d25..019ea92f 100644 --- a/cyclops-ctrl/mocks/ITemplateRepo.go +++ b/cyclops-ctrl/mocks/ITemplateRepo.go @@ -146,6 +146,64 @@ func (_c *ITemplateRepo_GetTemplateInitialValues_Call) RunAndReturn(run func(str return _c } +// GetTemplateRevisions provides a mock function with given fields: repo +func (_m *ITemplateRepo) GetTemplateRevisions(repo string) ([]string, error) { + ret := _m.Called(repo) + + if len(ret) == 0 { + panic("no return value specified for GetTemplateRevisions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok { + return rf(repo) + } + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(repo) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(repo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ITemplateRepo_GetTemplateRevisions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTemplateRevisions' +type ITemplateRepo_GetTemplateRevisions_Call struct { + *mock.Call +} + +// GetTemplateRevisions is a helper method to define mock.On call +// - repo string +func (_e *ITemplateRepo_Expecter) GetTemplateRevisions(repo interface{}) *ITemplateRepo_GetTemplateRevisions_Call { + return &ITemplateRepo_GetTemplateRevisions_Call{Call: _e.mock.On("GetTemplateRevisions", repo)} +} + +func (_c *ITemplateRepo_GetTemplateRevisions_Call) Run(run func(repo string)) *ITemplateRepo_GetTemplateRevisions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *ITemplateRepo_GetTemplateRevisions_Call) Return(_a0 []string, _a1 error) *ITemplateRepo_GetTemplateRevisions_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ITemplateRepo_GetTemplateRevisions_Call) RunAndReturn(run func(string) ([]string, error)) *ITemplateRepo_GetTemplateRevisions_Call { + _c.Call.Return(run) + return _c +} + // ReturnCache provides a mock function with no fields func (_m *ITemplateRepo) ReturnCache() *ristretto.Cache { ret := _m.Called() diff --git a/cyclops-ctrl/pkg/template/git.go b/cyclops-ctrl/pkg/template/git.go index cedd3ac7..cb4a3397 100644 --- a/cyclops-ctrl/pkg/template/git.go +++ b/cyclops-ctrl/pkg/template/git.go @@ -274,6 +274,31 @@ func (r Repo) LoadInitialTemplateValues(repoURL, path, commit string) (map[strin return initialValues, nil } +func (r Repo) listRemoteRefs(repo string, creds *auth.Credentials) ([]string, error) { + rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ + Name: "origin", + URLs: []string{repo}, + }) + + refs, err := rem.List(&git.ListOptions{ + PeelingOption: git.AppendPeeled, + Auth: httpBasicAuthCredentials(creds), + }) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("repo %s was not cloned successfully; authentication might be required; check if repository exists and you referenced it correctly", repo)) + } + + branches := make([]string, 0) + + for _, ref := range refs { + if ref.Name().IsBranch() { + branches = append(branches, ref.Name().Short()) + } + } + + return branches, nil +} + func resolveRef(repo, version string, creds *auth.Credentials) (string, error) { if len(version) == 0 { return resolveDefaultBranchRef(repo, creds) diff --git a/cyclops-ctrl/pkg/template/template.go b/cyclops-ctrl/pkg/template/template.go index 71472e70..f3114e20 100644 --- a/cyclops-ctrl/pkg/template/template.go +++ b/cyclops-ctrl/pkg/template/template.go @@ -2,6 +2,7 @@ package template import ( "fmt" + gitproviders2 "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/template/gitproviders" "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/auth" @@ -29,6 +30,7 @@ type ITemplateRepo interface { version string, source cyclopsv1alpha1.TemplateSourceType, ) (map[string]interface{}, error) + GetTemplateRevisions(repo string) ([]string, error) ReturnCache() *ristretto.Cache } @@ -179,6 +181,19 @@ func (r Repo) assumeTemplateSourceType(repo string) (cyclopsv1alpha1.TemplateSou return cyclopsv1alpha1.TemplateSourceTypeGit, nil } +func (r Repo) GetTemplateRevisions(repo string) ([]string, error) { + if !gitproviders2.IsGitHubSource(repo) { + return nil, nil + } + + creds, err := r.credResolver.RepoAuthCredentials(repo) + if err != nil { + return nil, err + } + + return r.listRemoteRefs(repo, creds) +} + func (r Repo) ReturnCache() *ristretto.Cache { return r.cache.ReturnCache() } diff --git a/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx b/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx index 392328f6..dfa65b73 100644 --- a/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx +++ b/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx @@ -16,6 +16,7 @@ import { Popover, Checkbox, Switch, + AutoComplete, } from "antd"; import axios from "axios"; import { @@ -66,6 +67,9 @@ const TemplateStore = () => { const [templateSourceTypeFilter, setTemplateSourceTypeFilter] = useState(sourceTypeFilter); + const [repoRevisions, setRepoRevisions] = useState([]); + const [repoRevisionOptions, setRepoRevisionOptions] = useState([]); + const [addForm] = Form.useForm(); const [editForm] = Form.useForm(); const [notificationApi, contextHolder] = notification.useNotification(); @@ -346,6 +350,27 @@ const TemplateStore = () => { ); }; + const fetchRepoRevisions = (e) => { + axios + .get(`/api/templates/revisions?repo=` + e.target.value) + .then((res) => { + setRepoRevisions(res.data); + }) + .catch(() => {}); + }; + + const handleRepoInput = (value) => { + if (repoRevisions.length === 0) { + setRepoRevisionOptions([]); + return; + } + + const filtered = repoRevisions + .filter((item) => item.toLowerCase().includes(value.toLowerCase())) + .map((item) => ({ value: item })); + setRepoRevisionOptions(filtered); + }; + return (
{error.message.length !== 0 && ( @@ -636,7 +661,7 @@ const TemplateStore = () => { rules={[{ required: true, message: "Repo URL is required" }]} style={{ marginBottom: "12px" }} > - + { name={["ref", "version"]} style={{ marginBottom: "12px" }} > - + + + {advancedTemplateGitOpsWrite()}