diff --git a/cmd/agola/cmd/org.go b/cmd/agola/cmd/org.go new file mode 100644 index 0000000..2fe4d7e --- /dev/null +++ b/cmd/agola/cmd/org.go @@ -0,0 +1,28 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +var cmdOrg = &cobra.Command{ + Use: "org", + Short: "org", +} + +func init() { + cmdAgola.AddCommand(cmdOrg) +} diff --git a/cmd/agola/cmd/orgcreate.go b/cmd/agola/cmd/orgcreate.go new file mode 100644 index 0000000..a4fbaf6 --- /dev/null +++ b/cmd/agola/cmd/orgcreate.go @@ -0,0 +1,67 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + + "github.com/pkg/errors" + "github.com/sorintlab/agola/internal/services/gateway/api" + + "github.com/spf13/cobra" +) + +var cmdOrgCreate = &cobra.Command{ + Use: "create", + Short: "create an organization", + Run: func(cmd *cobra.Command, args []string) { + if err := orgCreate(cmd, args); err != nil { + log.Fatalf("err: %v", err) + } + }, +} + +type orgCreateOptions struct { + name string +} + +var orgCreateOpts orgCreateOptions + +func init() { + flags := cmdOrgCreate.Flags() + + flags.StringVarP(&orgCreateOpts.name, "name", "n", "", "organization name") + + cmdOrgCreate.MarkFlagRequired("name") + + cmdOrg.AddCommand(cmdOrgCreate) +} + +func orgCreate(cmd *cobra.Command, args []string) error { + gwclient := api.NewClient(gatewayURL, token) + + req := &api.CreateOrgRequest{ + Name: orgCreateOpts.name, + } + + log.Infof("creating org") + org, _, err := gwclient.CreateOrg(context.TODO(), req) + if err != nil { + return errors.Wrapf(err, "failed to create org") + } + log.Infof("org %q created, ID: %q", org.Name, org.ID) + + return nil +} diff --git a/cmd/agola/cmd/orgdelete.go b/cmd/agola/cmd/orgdelete.go new file mode 100644 index 0000000..ec5789c --- /dev/null +++ b/cmd/agola/cmd/orgdelete.go @@ -0,0 +1,61 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + + "github.com/pkg/errors" + "github.com/sorintlab/agola/internal/services/gateway/api" + + "github.com/spf13/cobra" +) + +var cmdOrgDelete = &cobra.Command{ + Use: "delete", + Short: "delete an organization", + Run: func(cmd *cobra.Command, args []string) { + if err := orgDelete(cmd, args); err != nil { + log.Fatalf("err: %v", err) + } + }, +} + +type orgDeleteOptions struct { + name string +} + +var orgDeleteOpts orgDeleteOptions + +func init() { + flags := cmdOrgDelete.Flags() + + flags.StringVarP(&orgDeleteOpts.name, "name", "n", "", "organization name") + + cmdOrgDelete.MarkFlagRequired("name") + + cmdOrg.AddCommand(cmdOrgDelete) +} + +func orgDelete(cmd *cobra.Command, args []string) error { + gwclient := api.NewClient(gatewayURL, token) + + log.Infof("deleting organization %q", orgDeleteOpts.name) + if _, err := gwclient.DeleteOrg(context.TODO(), orgDeleteOpts.name); err != nil { + return errors.Wrapf(err, "failed to delete organization") + } + + return nil +} diff --git a/cmd/agola/cmd/projectcreate.go b/cmd/agola/cmd/projectcreate.go index bd9c74e..774e02e 100644 --- a/cmd/agola/cmd/projectcreate.go +++ b/cmd/agola/cmd/projectcreate.go @@ -19,6 +19,7 @@ import ( "github.com/pkg/errors" "github.com/sorintlab/agola/internal/services/gateway/api" + "github.com/sorintlab/agola/internal/services/types" "github.com/spf13/cobra" ) @@ -35,6 +36,7 @@ var cmdProjectCreate = &cobra.Command{ type projectCreateOptions struct { name string + organizationName string repoURL string remoteSourceName string skipSSHHostKeyCheck bool @@ -49,6 +51,7 @@ func init() { flags.StringVar(&projectCreateOpts.repoURL, "repo-url", "", "repository url") flags.StringVar(&projectCreateOpts.remoteSourceName, "remote-source", "", "remote source name") flags.BoolVarP(&projectCreateOpts.skipSSHHostKeyCheck, "skip-ssh-host-key-check", "s", false, "skip ssh host key check") + flags.StringVar(&projectCreateOpts.organizationName, "orgname", "", "organization name where the project should be created") cmdProjectCreate.MarkFlagRequired("name") cmdProjectCreate.MarkFlagRequired("repo-url") @@ -68,7 +71,14 @@ func projectCreate(cmd *cobra.Command, args []string) error { } log.Infof("creating project") - project, _, err := gwclient.CreateProject(context.TODO(), req) + + var project *types.Project + var err error + if projectCreateOpts.organizationName != "" { + project, _, err = gwclient.CreateOrgProject(context.TODO(), projectCreateOpts.organizationName, req) + } else { + project, _, err = gwclient.CreateCurrentUserProject(context.TODO(), req) + } if err != nil { return errors.Wrapf(err, "failed to create project") } diff --git a/cmd/agola/cmd/projectdelete.go b/cmd/agola/cmd/projectdelete.go index 5befd4d..57dfc45 100644 --- a/cmd/agola/cmd/projectdelete.go +++ b/cmd/agola/cmd/projectdelete.go @@ -34,7 +34,8 @@ var cmdProjectDelete = &cobra.Command{ } type projectDeleteOptions struct { - name string + name string + organizationName string } var projectDeleteOpts projectDeleteOptions @@ -43,6 +44,7 @@ func init() { flags := cmdProjectDelete.Flags() flags.StringVarP(&projectDeleteOpts.name, "name", "n", "", "project name") + flags.StringVar(&projectDeleteOpts.organizationName, "orgname", "", "organization name where the project should be deleted") cmdProjectDelete.MarkFlagRequired("name") @@ -53,7 +55,14 @@ func projectDelete(cmd *cobra.Command, args []string) error { gwclient := api.NewClient(gatewayURL, token) log.Infof("deleting project") - if _, err := gwclient.DeleteProject(context.TODO(), projectDeleteOpts.name); err != nil { + + var err error + if projectDeleteOpts.organizationName != "" { + _, err = gwclient.DeleteOrgProject(context.TODO(), projectDeleteOpts.organizationName, projectDeleteOpts.name) + } else { + _, err = gwclient.DeleteCurrentUserProject(context.TODO(), projectDeleteOpts.name) + } + if err != nil { return errors.Wrapf(err, "failed to delete project") } diff --git a/cmd/agola/cmd/projectlist.go b/cmd/agola/cmd/projectlist.go index 2da4259..5b4fcb9 100644 --- a/cmd/agola/cmd/projectlist.go +++ b/cmd/agola/cmd/projectlist.go @@ -57,7 +57,7 @@ func printProjects(projectsResponse *api.GetProjectsResponse) { func projectList(cmd *cobra.Command, args []string) error { gwclient := api.NewClient(gatewayURL, token) - projectsResponse, _, err := gwclient.GetProjects(context.TODO(), projectListOpts.start, projectListOpts.limit, false) + projectsResponse, _, err := gwclient.GetCurrentUserProjects(context.TODO(), projectListOpts.start, projectListOpts.limit, false) if err != nil { return err } diff --git a/cmd/agola/cmd/usercreate.go b/cmd/agola/cmd/usercreate.go index 21c3a67..1c531a1 100644 --- a/cmd/agola/cmd/usercreate.go +++ b/cmd/agola/cmd/usercreate.go @@ -45,8 +45,6 @@ func init() { flags.StringVarP(&userCreateOpts.username, "username", "n", "", "user name") cmdUserCreate.MarkFlagRequired("username") - cmdUserCreate.MarkFlagRequired("repo-url") - cmdUserCreate.MarkFlagRequired("token") cmdUser.AddCommand(cmdUserCreate) } diff --git a/internal/services/configstore/api/client.go b/internal/services/configstore/api/client.go index b7781de..d89a6e4 100644 --- a/internal/services/configstore/api/client.go +++ b/internal/services/configstore/api/client.go @@ -114,9 +114,9 @@ func (c *Client) GetProject(ctx context.Context, projectID string) (*types.Proje return project, resp, err } -func (c *Client) GetProjectByName(ctx context.Context, projectName string) (*types.Project, *http.Response, error) { +func (c *Client) GetProjectByName(ctx context.Context, ownerid, projectName string) (*types.Project, *http.Response, error) { project := new(types.Project) - resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/projects/%s", projectName), nil, jsonContent, nil, project) + resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/projects/%s/%s", ownerid, projectName), nil, jsonContent, nil, project) return project, resp, err } @@ -131,11 +131,11 @@ func (c *Client) CreateProject(ctx context.Context, project *types.Project) (*ty return project, resp, err } -func (c *Client) DeleteProject(ctx context.Context, projectName string) (*http.Response, error) { - return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projects/%s", projectName), nil, jsonContent, nil) +func (c *Client) DeleteProject(ctx context.Context, projectID string) (*http.Response, error) { + return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projects/%s", projectID), nil, jsonContent, nil) } -func (c *Client) GetProjects(ctx context.Context, start string, limit int, asc bool) ([]*types.Project, *http.Response, error) { +func (c *Client) GetOwnerProjects(ctx context.Context, ownerid, start string, limit int, asc bool) ([]*types.Project, *http.Response, error) { q := url.Values{} if start != "" { q.Add("start", start) @@ -148,7 +148,7 @@ func (c *Client) GetProjects(ctx context.Context, start string, limit int, asc b } projects := []*types.Project{} - resp, err := c.getParsedResponse(ctx, "GET", "/projects", q, jsonContent, nil, &projects) + resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/owner/%s/projects", ownerid), q, jsonContent, nil, &projects) return projects, resp, err } @@ -316,3 +316,47 @@ func (c *Client) CreateRemoteSource(ctx context.Context, rs *types.RemoteSource) func (c *Client) DeleteRemoteSource(ctx context.Context, name string) (*http.Response, error) { return c.getResponse(ctx, "DELETE", fmt.Sprintf("/remotesources/%s", name), nil, jsonContent, nil) } + +func (c *Client) CreateOrg(ctx context.Context, org *types.Organization) (*types.Organization, *http.Response, error) { + oj, err := json.Marshal(org) + if err != nil { + return nil, nil, err + } + + org = new(types.Organization) + resp, err := c.getParsedResponse(ctx, "PUT", "/orgs", nil, jsonContent, bytes.NewReader(oj), org) + return org, resp, err +} + +func (c *Client) DeleteOrg(ctx context.Context, orgname string) (*http.Response, error) { + return c.getResponse(ctx, "DELETE", fmt.Sprintf("/orgs/%s", orgname), nil, jsonContent, nil) +} + +func (c *Client) GetOrgs(ctx context.Context, start string, limit int, asc bool) ([]*types.Organization, *http.Response, error) { + q := url.Values{} + if start != "" { + q.Add("start", start) + } + if limit > 0 { + q.Add("limit", strconv.Itoa(limit)) + } + if asc { + q.Add("asc", "") + } + + orgs := []*types.Organization{} + resp, err := c.getParsedResponse(ctx, "GET", "/orgs", q, jsonContent, nil, &orgs) + return orgs, resp, err +} + +func (c *Client) GetOrg(ctx context.Context, orgID string) (*types.Organization, *http.Response, error) { + org := new(types.Organization) + resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/org/%s", orgID), nil, jsonContent, nil, org) + return org, resp, err +} + +func (c *Client) GetOrgByName(ctx context.Context, orgname string) (*types.Organization, *http.Response, error) { + org := new(types.Organization) + resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/orgs/%s", orgname), nil, jsonContent, nil, org) + return org, resp, err +} diff --git a/internal/services/configstore/api/org.go b/internal/services/configstore/api/org.go new file mode 100644 index 0000000..5529b5e --- /dev/null +++ b/internal/services/configstore/api/org.go @@ -0,0 +1,219 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/sorintlab/agola/internal/db" + "github.com/sorintlab/agola/internal/services/configstore/command" + "github.com/sorintlab/agola/internal/services/configstore/readdb" + "github.com/sorintlab/agola/internal/services/types" + + "github.com/gorilla/mux" + "go.uber.org/zap" +) + +type OrgHandler struct { + log *zap.SugaredLogger + readDB *readdb.ReadDB +} + +func NewOrgHandler(logger *zap.Logger, readDB *readdb.ReadDB) *OrgHandler { + return &OrgHandler{log: logger.Sugar(), readDB: readDB} +} + +func (h *OrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + orgID := vars["orgid"] + + var org *types.Organization + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + org, err = h.readDB.GetOrg(tx, orgID) + return err + }) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if org == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + if err := json.NewEncoder(w).Encode(org); err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type OrgByNameHandler struct { + log *zap.SugaredLogger + readDB *readdb.ReadDB +} + +func NewOrgByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *OrgByNameHandler { + return &OrgByNameHandler{log: logger.Sugar(), readDB: readDB} +} + +func (h *OrgByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + orgName := vars["orgname"] + + var org *types.Organization + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + org, err = h.readDB.GetOrgByName(tx, orgName) + return err + }) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if org == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + if err := json.NewEncoder(w).Encode(org); err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type CreateOrgHandler struct { + log *zap.SugaredLogger + ch *command.CommandHandler +} + +func NewCreateOrgHandler(logger *zap.Logger, ch *command.CommandHandler) *CreateOrgHandler { + return &CreateOrgHandler{log: logger.Sugar(), ch: ch} +} + +func (h *CreateOrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var req types.Organization + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + org, err := h.ch.CreateOrg(ctx, &req) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := json.NewEncoder(w).Encode(org); err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type DeleteOrgHandler struct { + log *zap.SugaredLogger + ch *command.CommandHandler +} + +func NewDeleteOrgHandler(logger *zap.Logger, ch *command.CommandHandler) *DeleteOrgHandler { + return &DeleteOrgHandler{log: logger.Sugar(), ch: ch} +} + +func (h *DeleteOrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.log.Infof("deleteorghandler") + ctx := r.Context() + + vars := mux.Vars(r) + orgName := vars["orgname"] + + if err := h.ch.DeleteOrg(ctx, orgName); err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } +} + +const ( + DefaultOrgsLimit = 10 + MaxOrgsLimit = 20 +) + +type OrgsHandler struct { + log *zap.SugaredLogger + readDB *readdb.ReadDB +} + +func NewOrgsHandler(logger *zap.Logger, readDB *readdb.ReadDB) *OrgsHandler { + return &OrgsHandler{log: logger.Sugar(), readDB: readDB} +} + +func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + limitS := query.Get("limit") + limit := DefaultOrgsLimit + if limitS != "" { + var err error + limit, err = strconv.Atoi(limitS) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } + } + if limit < 0 { + http.Error(w, "limit must be greater or equal than 0", http.StatusBadRequest) + return + } + if limit > MaxOrgsLimit { + limit = MaxOrgsLimit + } + asc := false + if _, ok := query["asc"]; ok { + asc = true + } + + start := query.Get("start") + + var orgs []*types.Organization + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + orgs, err = h.readDB.GetOrgs(tx, start, limit, asc) + return err + }) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := json.NewEncoder(w).Encode(orgs); err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/internal/services/configstore/api/project.go b/internal/services/configstore/api/project.go index e81a4c7..e7caac6 100644 --- a/internal/services/configstore/api/project.go +++ b/internal/services/configstore/api/project.go @@ -28,16 +28,16 @@ import ( "go.uber.org/zap" ) -type GetProjectHandler struct { +type ProjectHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetProjectHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetProjectHandler { - return &GetProjectHandler{log: logger.Sugar(), readDB: readDB} +func NewProjectHandler(logger *zap.Logger, readDB *readdb.ReadDB) *ProjectHandler { + return &ProjectHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) projectID := vars["projectid"] @@ -63,23 +63,24 @@ func (h *GetProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type GetProjectByNameHandler struct { +type ProjectByNameHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetProjectByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetProjectByNameHandler { - return &GetProjectByNameHandler{log: logger.Sugar(), readDB: readDB} +func NewProjectByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *ProjectByNameHandler { + return &ProjectByNameHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetProjectByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *ProjectByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) + ownerID := vars["ownerid"] projectName := vars["projectname"] var project *types.Project err := h.readDB.Do(func(tx *db.Tx) error { var err error - project, err = h.readDB.GetProjectByName(tx, projectName) + project, err = h.readDB.GetOwnerProjectByName(tx, ownerID, projectName) return err }) if err != nil { @@ -144,9 +145,9 @@ func (h *DeleteProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) ctx := r.Context() vars := mux.Vars(r) - projectName := vars["projectname"] + projectID := vars["projectid"] - if err := h.ch.DeleteProject(ctx, projectName); err != nil { + if err := h.ch.DeleteProject(ctx, projectID); err != nil { h.log.Errorf("err: %+v", err) http.Error(w, err.Error(), http.StatusBadRequest) return @@ -168,6 +169,9 @@ func NewProjectsHandler(logger *zap.Logger, readDB *readdb.ReadDB) *ProjectsHand } func (h *ProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + ownerID := vars["ownerid"] + query := r.URL.Query() limitS := query.Get("limit") @@ -194,7 +198,12 @@ func (h *ProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := query.Get("start") - projects, err := h.readDB.GetProjects(start, limit, asc) + var projects []*types.Project + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + projects, err = h.readDB.GetOwnerProjects(tx, ownerID, start, limit, asc) + return err + }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/services/configstore/api/remotesource.go b/internal/services/configstore/api/remotesource.go index 9ccfcd2..01fb5e5 100644 --- a/internal/services/configstore/api/remotesource.go +++ b/internal/services/configstore/api/remotesource.go @@ -28,16 +28,16 @@ import ( "go.uber.org/zap" ) -type GetRemoteSourceHandler struct { +type RemoteSourceHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetRemoteSourceHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetRemoteSourceHandler { - return &GetRemoteSourceHandler{log: logger.Sugar(), readDB: readDB} +func NewRemoteSourceHandler(logger *zap.Logger, readDB *readdb.ReadDB) *RemoteSourceHandler { + return &RemoteSourceHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetRemoteSourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *RemoteSourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) remoteSourceID := vars["id"] @@ -63,16 +63,16 @@ func (h *GetRemoteSourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques } } -type GetRemoteSourceByNameHandler struct { +type RemoteSourceByNameHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetRemoteSourceByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetRemoteSourceByNameHandler { - return &GetRemoteSourceByNameHandler{log: logger.Sugar(), readDB: readDB} +func NewRemoteSourceByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *RemoteSourceByNameHandler { + return &RemoteSourceByNameHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetRemoteSourceByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *RemoteSourceByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) remoteSourceName := vars["name"] diff --git a/internal/services/configstore/api/user.go b/internal/services/configstore/api/user.go index 6901572..1bc3bc1 100644 --- a/internal/services/configstore/api/user.go +++ b/internal/services/configstore/api/user.go @@ -29,16 +29,16 @@ import ( "go.uber.org/zap" ) -type GetUserHandler struct { +type UserHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetUserHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetUserHandler { - return &GetUserHandler{log: logger.Sugar(), readDB: readDB} +func NewUserHandler(logger *zap.Logger, readDB *readdb.ReadDB) *UserHandler { + return &UserHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userID := vars["userid"] @@ -66,16 +66,16 @@ func (h *GetUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -type GetUserByNameHandler struct { +type UserByNameHandler struct { log *zap.SugaredLogger readDB *readdb.ReadDB } -func NewGetUserByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *GetUserByNameHandler { - return &GetUserByNameHandler{log: logger.Sugar(), readDB: readDB} +func NewUserByNameHandler(logger *zap.Logger, readDB *readdb.ReadDB) *UserByNameHandler { + return &UserByNameHandler{log: logger.Sugar(), readDB: readDB} } -func (h *GetUserByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *UserByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userName := vars["username"] diff --git a/internal/services/configstore/command/command.go b/internal/services/configstore/command/command.go index 9172069..7ef2715 100644 --- a/internal/services/configstore/command/command.go +++ b/internal/services/configstore/command/command.go @@ -48,12 +48,20 @@ func (s *CommandHandler) CreateProject(ctx context.Context, project *types.Proje if project.Name == "" { return nil, errors.Errorf("project name required") } + if project.OwnerType == "" { + return nil, errors.Errorf("project ownertype required") + } + if project.OwnerID == "" { + return nil, errors.Errorf("project ownerid required") + } + if !types.IsValidOwnerType(project.OwnerType) { + return nil, errors.Errorf("invalid project ownertype %q", project.OwnerType) + } var cgt *wal.ChangeGroupsUpdateToken - cgNames := []string{project.Name} + cgNames := []string{project.OwnerID} // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) @@ -61,13 +69,32 @@ func (s *CommandHandler) CreateProject(ctx context.Context, project *types.Proje return err } + // check owner exists + switch project.OwnerType { + case types.OwnerTypeUser: + user, err := s.readDB.GetUser(tx, project.OwnerID) + if err != nil { + return err + } + if user == nil { + return errors.Errorf("user id %q doesn't exist", project.OwnerID) + } + case types.OwnerTypeOrganization: + org, err := s.readDB.GetOrg(tx, project.OwnerID) + if err != nil { + return err + } + if org == nil { + return errors.Errorf("organization id %q doesn't exist", project.OwnerID) + } + } // check duplicate project name - p, err := s.readDB.GetProjectByName(tx, project.Name) + p, err := s.readDB.GetOwnerProjectByName(tx, project.OwnerID, project.Name) if err != nil { return err } if p != nil { - return errors.Errorf("project %q already exists", p.Name) + return errors.Errorf("project with name %q for %s with id %q already exists", p.Name, project.OwnerType, project.OwnerID) } return nil }) @@ -93,14 +120,13 @@ func (s *CommandHandler) CreateProject(ctx context.Context, project *types.Proje return project, err } -func (s *CommandHandler) DeleteProject(ctx context.Context, projectName string) error { +func (s *CommandHandler) DeleteProject(ctx context.Context, projectID string) error { var project *types.Project var cgt *wal.ChangeGroupsUpdateToken - cgNames := []string{project.Name} + cgNames := []string{project.OwnerID} // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) @@ -109,12 +135,12 @@ func (s *CommandHandler) DeleteProject(ctx context.Context, projectName string) } // check project existance - project, err = s.readDB.GetProjectByName(tx, projectName) + project, err = s.readDB.GetProject(tx, projectID) if err != nil { return err } if project == nil { - return errors.Errorf("project %q doesn't exist", projectName) + return errors.Errorf("project %q doesn't exist", projectID) } return nil }) @@ -142,7 +168,6 @@ func (s *CommandHandler) CreateUser(ctx context.Context, user *types.User) (*typ cgNames := []string{user.UserName} // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) @@ -156,7 +181,7 @@ func (s *CommandHandler) CreateUser(ctx context.Context, user *types.User) (*typ return err } if u != nil { - return errors.Errorf("user %q already exists", u.UserName) + return errors.Errorf("user with name %q already exists", u.UserName) } return nil }) @@ -166,7 +191,7 @@ func (s *CommandHandler) CreateUser(ctx context.Context, user *types.User) (*typ user.ID = uuid.NewV4().String() - pcj, err := json.Marshal(user) + userj, err := json.Marshal(user) if err != nil { return nil, errors.Wrapf(err, "failed to marshal user") } @@ -174,7 +199,7 @@ func (s *CommandHandler) CreateUser(ctx context.Context, user *types.User) (*typ { ActionType: wal.ActionTypePut, Path: common.StorageUserFile(user.ID), - Data: pcj, + Data: userj, }, } @@ -189,7 +214,6 @@ func (s *CommandHandler) DeleteUser(ctx context.Context, userName string) error cgNames := []string{user.UserName} // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) @@ -248,7 +272,6 @@ func (s *CommandHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequ var cgt *wal.ChangeGroupsUpdateToken // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error user, err = s.readDB.GetUserByName(tx, req.UserName) @@ -294,7 +317,7 @@ func (s *CommandHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequ user.LinkedAccounts[la.ID] = la - pcj, err := json.Marshal(user) + userj, err := json.Marshal(user) if err != nil { return nil, errors.Wrapf(err, "failed to marshal user") } @@ -302,7 +325,7 @@ func (s *CommandHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequ { ActionType: wal.ActionTypePut, Path: common.StorageUserFile(user.ID), - Data: pcj, + Data: userj, }, } @@ -323,7 +346,6 @@ func (s *CommandHandler) DeleteUserLA(ctx context.Context, userName, laID string var cgt *wal.ChangeGroupsUpdateToken // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error user, err = s.readDB.GetUserByName(tx, userName) @@ -353,7 +375,7 @@ func (s *CommandHandler) DeleteUserLA(ctx context.Context, userName, laID string delete(user.LinkedAccounts, laID) - pcj, err := json.Marshal(user) + userj, err := json.Marshal(user) if err != nil { return errors.Wrapf(err, "failed to marshal user") } @@ -361,7 +383,7 @@ func (s *CommandHandler) DeleteUserLA(ctx context.Context, userName, laID string { ActionType: wal.ActionTypePut, Path: common.StorageUserFile(user.ID), - Data: pcj, + Data: userj, }, } @@ -390,7 +412,6 @@ func (s *CommandHandler) UpdateUserLA(ctx context.Context, req *UpdateUserLARequ var cgt *wal.ChangeGroupsUpdateToken // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error user, err = s.readDB.GetUserByName(tx, req.UserName) @@ -514,7 +535,6 @@ func (s *CommandHandler) CreateRemoteSource(ctx context.Context, remoteSource *t cgNames := []string{remoteSource.Name} // must do all the check in a single transaction to avoid concurrent changes - // since the use token is related to the transaction time err := s.readDB.Do(func(tx *db.Tx) error { var err error cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) @@ -593,3 +613,103 @@ func (s *CommandHandler) DeleteRemoteSource(ctx context.Context, remoteSourceNam _, err = s.wal.WriteWal(ctx, actions, cgt) return err } + +func (s *CommandHandler) CreateOrg(ctx context.Context, org *types.Organization) (*types.Organization, error) { + if org.Name == "" { + return nil, errors.Errorf("org name required") + } + + var cgt *wal.ChangeGroupsUpdateToken + cgNames := []string{org.Name} + + // must do all the check in a single transaction to avoid concurrent changes + err := s.readDB.Do(func(tx *db.Tx) error { + var err error + cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + // check duplicate org name + u, err := s.readDB.GetOrgByName(tx, org.Name) + if err != nil { + return err + } + if u != nil { + return errors.Errorf("org %q already exists", u.Name) + } + return nil + }) + if err != nil { + return nil, err + } + + org.ID = uuid.NewV4().String() + + orgj, err := json.Marshal(org) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal org") + } + actions := []*wal.Action{ + { + ActionType: wal.ActionTypePut, + Path: common.StorageOrgFile(org.ID), + Data: orgj, + }, + } + + _, err = s.wal.WriteWal(ctx, actions, cgt) + return org, err +} + +func (s *CommandHandler) DeleteOrg(ctx context.Context, orgName string) error { + var org *types.Organization + var projects []*types.Project + + var cgt *wal.ChangeGroupsUpdateToken + cgNames := []string{org.ID} + + // must do all the check in a single transaction to avoid concurrent changes + err := s.readDB.Do(func(tx *db.Tx) error { + var err error + cgt, err = s.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + // check org existance + org, err = s.readDB.GetOrgByName(tx, orgName) + if err != nil { + return err + } + if org == nil { + return errors.Errorf("org %q doesn't exist", orgName) + } + // get org projects + projects, err = s.readDB.GetOwnerProjects(tx, org.ID, "", 0, false) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + actions := []*wal.Action{ + { + ActionType: wal.ActionTypeDelete, + Path: common.StorageOrgFile(org.ID), + }, + } + // delete all org projects + for _, project := range projects { + actions = append(actions, &wal.Action{ + ActionType: wal.ActionTypeDelete, + Path: common.StorageProjectFile(project.ID), + }) + } + + _, err = s.wal.WriteWal(ctx, actions, cgt) + return err +} diff --git a/internal/services/configstore/common/common.go b/internal/services/configstore/common/common.go index b39fa38..11ade36 100644 --- a/internal/services/configstore/common/common.go +++ b/internal/services/configstore/common/common.go @@ -24,6 +24,7 @@ var ( StorageDataDir = "data" StorageProjectsDir = path.Join(StorageDataDir, "projects") StorageUsersDir = path.Join(StorageDataDir, "users") + StorageOrgsDir = path.Join(StorageDataDir, "orgs") StorageRemoteSourcesDir = path.Join(StorageDataDir, "remotesources") ) @@ -39,6 +40,10 @@ func StorageUserFile(userID string) string { return path.Join(StorageUsersDir, userID) } +func StorageOrgFile(orgID string) string { + return path.Join(StorageOrgsDir, orgID) +} + func StorageRemoteSourceFile(userID string) string { return path.Join(StorageRemoteSourcesDir, userID) } @@ -48,6 +53,7 @@ type ConfigType string const ( ConfigTypeProject ConfigType = "project" ConfigTypeUser ConfigType = "user" + ConfigTypeOrg ConfigType = "org" ConfigTypeRemoteSource ConfigType = "remotesource" ) @@ -58,6 +64,8 @@ func PathToTypeID(p string) (ConfigType, string) { configType = ConfigTypeProject case StorageUsersDir: configType = ConfigTypeUser + case StorageOrgsDir: + configType = ConfigTypeOrg case StorageRemoteSourcesDir: configType = ConfigTypeRemoteSource default: diff --git a/internal/services/configstore/configstore.go b/internal/services/configstore/configstore.go index 6fff4fa..48fda4c 100644 --- a/internal/services/configstore/configstore.go +++ b/internal/services/configstore/configstore.go @@ -109,15 +109,15 @@ func (s *ConfigStore) Run(ctx context.Context) error { corsAllowedOriginsOptions := ghandlers.AllowedOrigins([]string{"*"}) corsHandler = ghandlers.CORS(corsAllowedMethodsOptions, corsAllowedHeadersOptions, corsAllowedOriginsOptions) - getProjectHandler := api.NewGetProjectHandler(logger, s.readDB) + projectHandler := api.NewProjectHandler(logger, s.readDB) projectsHandler := api.NewProjectsHandler(logger, s.readDB) - getProjectByNameHandler := api.NewGetProjectByNameHandler(logger, s.readDB) + projectByNameHandler := api.NewProjectByNameHandler(logger, s.readDB) createProjectHandler := api.NewCreateProjectHandler(logger, s.ch) deleteProjectHandler := api.NewDeleteProjectHandler(logger, s.ch) - getUserHandler := api.NewGetUserHandler(logger, s.readDB) + userHandler := api.NewUserHandler(logger, s.readDB) usersHandler := api.NewUsersHandler(logger, s.readDB) - getUserByNameHandler := api.NewGetUserByNameHandler(logger, s.readDB) + userByNameHandler := api.NewUserByNameHandler(logger, s.readDB) createUserHandler := api.NewCreateUserHandler(logger, s.ch) deleteUserHandler := api.NewDeleteUserHandler(logger, s.ch) @@ -127,25 +127,31 @@ func (s *ConfigStore) Run(ctx context.Context) error { createUserTokenHandler := api.NewCreateUserTokenHandler(logger, s.ch) - getRemoteSourceHandler := api.NewGetRemoteSourceHandler(logger, s.readDB) + orgHandler := api.NewOrgHandler(logger, s.readDB) + orgsHandler := api.NewOrgsHandler(logger, s.readDB) + orgByNameHandler := api.NewOrgByNameHandler(logger, s.readDB) + createOrgHandler := api.NewCreateOrgHandler(logger, s.ch) + deleteOrgHandler := api.NewDeleteOrgHandler(logger, s.ch) + + remoteSourceHandler := api.NewRemoteSourceHandler(logger, s.readDB) remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, s.readDB) - getRemoteSourceByNameHandler := api.NewGetRemoteSourceByNameHandler(logger, s.readDB) + remoteSourceByNameHandler := api.NewRemoteSourceByNameHandler(logger, s.readDB) createRemoteSourceHandler := api.NewCreateRemoteSourceHandler(logger, s.ch) deleteRemoteSourceHandler := api.NewDeleteRemoteSourceHandler(logger, s.ch) router := mux.NewRouter() apirouter := router.PathPrefix("/api/v1alpha").Subrouter() - apirouter.Handle("/project/{projectid}", getProjectHandler).Methods("GET") - apirouter.Handle("/projects", projectsHandler).Methods("GET") + apirouter.Handle("/project/{projectid}", projectHandler).Methods("GET") + apirouter.Handle("/owner/{ownerid}/projects", projectsHandler).Methods("GET") + apirouter.Handle("/projects/{ownerid}/{projectname}", projectByNameHandler).Methods("GET") apirouter.Handle("/projects", createProjectHandler).Methods("PUT") - apirouter.Handle("/projects/{projectname}", getProjectByNameHandler).Methods("GET") - apirouter.Handle("/projects/{projectname}", deleteProjectHandler).Methods("DELETE") + apirouter.Handle("/projects/{projectid}", deleteProjectHandler).Methods("DELETE") - apirouter.Handle("/user/{userid}", getUserHandler).Methods("GET") + apirouter.Handle("/user/{userid}", userHandler).Methods("GET") apirouter.Handle("/users", usersHandler).Methods("GET") apirouter.Handle("/users", createUserHandler).Methods("PUT") - apirouter.Handle("/users/{username}", getUserByNameHandler).Methods("GET") + apirouter.Handle("/users/{username}", userByNameHandler).Methods("GET") apirouter.Handle("/users/{username}", deleteUserHandler).Methods("DELETE") apirouter.Handle("/users/{username}/linkedaccounts", createUserLAHandler).Methods("PUT") @@ -153,10 +159,16 @@ func (s *ConfigStore) Run(ctx context.Context) error { apirouter.Handle("/users/{username}/linkedaccounts/{laid}", updateUserLAHandler).Methods("PUT") apirouter.Handle("/users/{username}/tokens", createUserTokenHandler).Methods("PUT") - apirouter.Handle("/remotesource/{id}", getRemoteSourceHandler).Methods("GET") + apirouter.Handle("/org/{orgid}", orgHandler).Methods("GET") + apirouter.Handle("/orgs", orgsHandler).Methods("GET") + apirouter.Handle("/orgs", createOrgHandler).Methods("PUT") + apirouter.Handle("/orgs/{orgname}", orgByNameHandler).Methods("GET") + apirouter.Handle("/orgs/{orgname}", deleteOrgHandler).Methods("DELETE") + + apirouter.Handle("/remotesource/{id}", remoteSourceHandler).Methods("GET") apirouter.Handle("/remotesources", remoteSourcesHandler).Methods("GET") apirouter.Handle("/remotesources", createRemoteSourceHandler).Methods("PUT") - apirouter.Handle("/remotesources/{name}", getRemoteSourceByNameHandler).Methods("GET") + apirouter.Handle("/remotesources/{name}", remoteSourceByNameHandler).Methods("GET") apirouter.Handle("/remotesources/{name}", deleteRemoteSourceHandler).Methods("DELETE") mainrouter := mux.NewRouter() diff --git a/internal/services/configstore/configstore_test.go b/internal/services/configstore/configstore_test.go index fc49ff5..b0e1e38 100644 --- a/internal/services/configstore/configstore_test.go +++ b/internal/services/configstore/configstore_test.go @@ -21,9 +21,11 @@ import ( "net" "os" "reflect" + "sync" "testing" "time" + "github.com/sorintlab/agola/internal/db" "github.com/sorintlab/agola/internal/services/config" "github.com/sorintlab/agola/internal/services/types" "github.com/sorintlab/agola/internal/testutil" @@ -50,6 +52,60 @@ func shutdownEtcd(tetcd *testutil.TestEmbeddedEtcd) { } } +func setupConfigstore(t *testing.T, ctx context.Context, dir string) (*ConfigStore, *testutil.TestEtcd) { + etcdDir, err := ioutil.TempDir(dir, "etcd") + tetcd := setupEtcd(t, etcdDir) + + listenAddress, port, err := testutil.GetFreePort(true, false) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + ltsDir, err := ioutil.TempDir(dir, "lts") + csDir, err := ioutil.TempDir(dir, "cs") + + baseConfig := config.ConfigStore{ + Etcd: config.Etcd{ + Endpoints: tetcd.Endpoint, + }, + LTS: config.LTS{ + Type: config.LTSTypePosix, + Path: ltsDir, + }, + Web: config.Web{}, + } + csConfig := baseConfig + csConfig.DataDir = csDir + csConfig.Web.ListenAddress = net.JoinHostPort(listenAddress, port) + + cs, err := NewConfigStore(ctx, &csConfig) + if err != nil { + t.Fatalf("err: %v", err) + } + + return cs, tetcd +} + +func getProjects(cs *ConfigStore) ([]*types.Project, error) { + var projects []*types.Project + err := cs.readDB.Do(func(tx *db.Tx) error { + var err error + projects, err = cs.readDB.GetProjects(tx, "", 0, true) + return err + }) + return projects, err +} + +func getUsers(cs *ConfigStore) ([]*types.User, error) { + var users []*types.User + err := cs.readDB.Do(func(tx *db.Tx) error { + var err error + users, err = cs.readDB.GetUsers(tx, "", 0, true) + return err + }) + return users, err +} + func TestResync(t *testing.T) { dir, err := ioutil.TempDir("", "agola") if err != nil { @@ -126,7 +182,7 @@ func TestResync(t *testing.T) { time.Sleep(1 * time.Second) for i := 0; i < 10; i++ { - if _, err := cs1.ch.CreateProject(ctx, &types.Project{Name: fmt.Sprintf("project%d", i)}); err != nil { + if _, err := cs1.ch.CreateUser(ctx, &types.User{UserName: fmt.Sprintf("user%d", i)}); err != nil { t.Fatalf("err: %v", err) } time.Sleep(200 * time.Millisecond) @@ -140,7 +196,7 @@ func TestResync(t *testing.T) { // Do some more changes for i := 11; i < 20; i++ { - if _, err := cs1.ch.CreateProject(ctx, &types.Project{Name: fmt.Sprintf("project%d", i)}); err != nil { + if _, err := cs1.ch.CreateUser(ctx, &types.User{UserName: fmt.Sprintf("user%d", i)}); err != nil { t.Fatalf("err: %v", err) } time.Sleep(200 * time.Millisecond) @@ -165,22 +221,22 @@ func TestResync(t *testing.T) { time.Sleep(5 * time.Second) - projects1, err := cs1.readDB.GetProjects("", 0, true) + users1, err := getUsers(cs1) if err != nil { t.Fatalf("err: %v", err) } - projects2, err := cs2.readDB.GetProjects("", 0, true) + users2, err := getUsers(cs2) if err != nil { t.Fatalf("err: %v", err) } - if !compareProjects(projects1, projects2) { - t.Logf("len(projects1): %d", len(projects1)) - t.Logf("len(projects2): %d", len(projects2)) - t.Logf("projects1: %s", util.Dump(projects1)) - t.Logf("projects2: %s", util.Dump(projects2)) - t.Fatalf("projects are different between the two readdbs") + if !compareUsers(users1, users2) { + t.Logf("len(users1): %d", len(users1)) + t.Logf("len(users2): %d", len(users2)) + t.Logf("users1: %s", util.Dump(users1)) + t.Logf("users2: %s", util.Dump(users2)) + t.Fatalf("users are different between the two readdbs") } // start cs3, since it's a new instance it should do a full resync @@ -198,35 +254,220 @@ func TestResync(t *testing.T) { time.Sleep(5 * time.Second) - projects1, err = cs1.readDB.GetProjects("", 0, true) + users1, err = getUsers(cs1) if err != nil { t.Fatalf("err: %v", err) } - projects3, err := cs3.readDB.GetProjects("", 0, true) + users3, err := getUsers(cs3) if err != nil { t.Fatalf("err: %v", err) } - if !compareProjects(projects1, projects3) { - t.Logf("len(projects1): %d", len(projects1)) - t.Logf("len(projects3): %d", len(projects3)) - t.Logf("projects1: %s", util.Dump(projects1)) - t.Logf("projects3: %s", util.Dump(projects3)) - t.Fatalf("projects are different between the two readdbs") + if !compareUsers(users1, users3) { + t.Logf("len(users1): %d", len(users1)) + t.Logf("len(users3): %d", len(users3)) + t.Logf("users1: %s", util.Dump(users1)) + t.Logf("users3: %s", util.Dump(users3)) + t.Fatalf("users are different between the two readdbs") } } -func compareProjects(p1, p2 []*types.Project) bool { - p1ids := map[string]struct{}{} - p2ids := map[string]struct{}{} +func compareUsers(u1, u2 []*types.User) bool { + u1ids := map[string]struct{}{} + u2ids := map[string]struct{}{} - for _, p := range p1 { - p1ids[p.ID] = struct{}{} + for _, u := range u1 { + u1ids[u.ID] = struct{}{} } - for _, p := range p2 { - p2ids[p.ID] = struct{}{} + for _, u := range u2 { + u2ids[u.ID] = struct{}{} } - return reflect.DeepEqual(p1ids, p2ids) + return reflect.DeepEqual(u1ids, u2ids) +} + +func TestUser(t *testing.T) { + dir, err := ioutil.TempDir("", "agola") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + ctx := context.Background() + + cs, tetcd := setupConfigstore(t, ctx, dir) + defer shutdownEtcd(tetcd) + + t.Logf("starting cs") + go func() { + if err := cs.Run(ctx); err != nil { + t.Fatalf("err: %v", err) + } + }() + + // TODO(sgotti) change the sleep with a real check that all is ready + time.Sleep(2 * time.Second) + + t.Run("create user", func(t *testing.T) { + _, err := cs.ch.CreateUser(ctx, &types.User{UserName: "user01"}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + }) + + // TODO(sgotti) change the sleep with a real check that user is in readdb + time.Sleep(2 * time.Second) + + t.Run("create duplicated user", func(t *testing.T) { + expectedErr := fmt.Sprintf("user with name %q already exists", "user01") + _, err := cs.ch.CreateUser(ctx, &types.User{UserName: "user01"}) + if err == nil { + t.Fatalf("expected error %v, got nil err", expectedErr) + } + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("concurrent user with same name creation", func(t *testing.T) { + prevUsers, err := getUsers(cs) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go cs.ch.CreateUser(ctx, &types.User{UserName: "user02"}) + wg.Done() + } + wg.Wait() + + time.Sleep(5 * time.Second) + + users, err := getUsers(cs) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + if len(users) != len(prevUsers)+1 { + t.Fatalf("expected %d users, got %d", len(prevUsers)+1, len(users)) + } + }) +} + +func TestProject(t *testing.T) { + dir, err := ioutil.TempDir("", "agola") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + ctx := context.Background() + + cs, tetcd := setupConfigstore(t, ctx, dir) + defer shutdownEtcd(tetcd) + + t.Logf("starting cs") + go func() { + if err := cs.Run(ctx); err != nil { + t.Fatalf("err: %v", err) + } + }() + + // TODO(sgotti) change the sleep with a real check that all is ready + time.Sleep(2 * time.Second) + + user, err := cs.ch.CreateUser(ctx, &types.User{UserName: "user01"}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + org, err := cs.ch.CreateOrg(ctx, &types.Organization{Name: "org01"}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // TODO(sgotti) change the sleep with a real check that user is in readdb + time.Sleep(2 * time.Second) + + t.Run("create project with owner type user", func(t *testing.T) { + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "user", OwnerID: user.ID}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + }) + t.Run("create project with owner type org", func(t *testing.T) { + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "organization", OwnerID: org.ID}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + }) + t.Run("create duplicated project for user", func(t *testing.T) { + expectedErr := fmt.Sprintf("project with name %q for user with id %q already exists", "project01", user.ID) + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "user", OwnerID: user.ID}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("create duplicated project for org", func(t *testing.T) { + expectedErr := fmt.Sprintf("project with name %q for organization with id %q already exists", "project01", org.ID) + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "organization", OwnerID: org.ID}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("create project with owner as unexistent user", func(t *testing.T) { + expectedErr := `user id "unexistentid" doesn't exist` + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "user", OwnerID: "unexistentid"}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("create project with owner as unexistent org", func(t *testing.T) { + expectedErr := `organization id "unexistentid" doesn't exist` + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "organization", OwnerID: "unexistentid"}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("create project without ownertype specified", func(t *testing.T) { + expectedErr := "project ownertype required" + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01"}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + t.Run("create project without ownerid specified", func(t *testing.T) { + expectedErr := "project ownerid required" + _, err := cs.ch.CreateProject(ctx, &types.Project{Name: "project01", OwnerType: "organization"}) + if err.Error() != expectedErr { + t.Fatalf("expected err %v, got err: %v", expectedErr, err) + } + }) + + t.Run("concurrent project with same name creation", func(t *testing.T) { + prevProjects, err := getProjects(cs) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go cs.ch.CreateProject(ctx, &types.Project{Name: "project02", OwnerType: "user", OwnerID: user.ID}) + wg.Done() + } + wg.Wait() + + time.Sleep(1 * time.Second) + + projects, err := getProjects(cs) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + if len(projects) != len(prevProjects)+1 { + t.Fatalf("expected %d projects, got %d", len(prevProjects)+1, len(projects)) + } + }) } diff --git a/internal/services/configstore/readdb/migration.go b/internal/services/configstore/readdb/migration.go index 4a478cb..078bab3 100644 --- a/internal/services/configstore/readdb/migration.go +++ b/internal/services/configstore/readdb/migration.go @@ -28,11 +28,13 @@ var Stmts = []string{ "create index project_name on project(name)", "create table user (id uuid, name varchar, data bytea, PRIMARY KEY (id))", + "create index user_name on user(name)", "create table user_token (tokenvalue varchar, userid uuid, PRIMARY KEY (tokenvalue, userid))", - "create table remotesource (id uuid, name varchar, data bytea, PRIMARY KEY (id))", + "create table org (id uuid, name varchar, data bytea, PRIMARY KEY (id))", + "create index org_name on org(name)", - "create table projectsource (id uuid, name varchar, data bytea, PRIMARY KEY (id))", + "create table remotesource (id uuid, name varchar, data bytea, PRIMARY KEY (id))", "create table linkedaccount_user (id uuid, remotesourceid uuid, userid uuid, remoteuserid uuid, PRIMARY KEY (id), FOREIGN KEY(userid) REFERENCES user(id))", diff --git a/internal/services/configstore/readdb/org.go b/internal/services/configstore/readdb/org.go new file mode 100644 index 0000000..0a3c669 --- /dev/null +++ b/internal/services/configstore/readdb/org.go @@ -0,0 +1,185 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package readdb + +import ( + "database/sql" + "encoding/json" + + "github.com/sorintlab/agola/internal/db" + "github.com/sorintlab/agola/internal/services/types" + "github.com/sorintlab/agola/internal/util" + + sq "github.com/Masterminds/squirrel" + "github.com/pkg/errors" +) + +var ( + orgSelect = sb.Select("org.id", "org.data").From("org") + orgInsert = sb.Insert("org").Columns("id", "name", "data") +) + +func (r *ReadDB) insertOrg(tx *db.Tx, data []byte) error { + org := types.Organization{} + if err := json.Unmarshal(data, &org); err != nil { + return errors.Wrap(err, "failed to unmarshal org") + } + r.log.Infof("inserting org: %s", util.Dump(org)) + // poor man insert or update... + if err := r.deleteOrg(tx, org.ID); err != nil { + return err + } + q, args, err := orgInsert.Values(org.ID, org.Name, data).ToSql() + if err != nil { + return errors.Wrap(err, "failed to build query") + } + if _, err := tx.Exec(q, args...); err != nil { + return errors.Wrap(err, "failed to insert org") + } + + return nil +} + +func (r *ReadDB) deleteOrg(tx *db.Tx, orgID string) error { + if _, err := tx.Exec("delete from org where id = $1", orgID); err != nil { + return errors.Wrap(err, "failed to delete org") + } + return nil +} + +func (r *ReadDB) GetOrg(tx *db.Tx, orgID string) (*types.Organization, error) { + q, args, err := orgSelect.Where(sq.Eq{"id": orgID}).ToSql() + r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) + if err != nil { + return nil, errors.Wrap(err, "failed to build query") + } + + orgs, _, err := fetchOrgs(tx, q, args...) + if err != nil { + return nil, errors.WithStack(err) + } + if len(orgs) > 1 { + return nil, errors.Errorf("too many rows returned") + } + if len(orgs) == 0 { + return nil, nil + } + return orgs[0], nil +} + +func (r *ReadDB) GetOrgByName(tx *db.Tx, name string) (*types.Organization, error) { + q, args, err := orgSelect.Where(sq.Eq{"name": name}).ToSql() + r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) + if err != nil { + return nil, errors.Wrap(err, "failed to build query") + } + + orgs, _, err := fetchOrgs(tx, q, args...) + if err != nil { + return nil, errors.WithStack(err) + } + if len(orgs) > 1 { + return nil, errors.Errorf("too many rows returned") + } + if len(orgs) == 0 { + return nil, nil + } + return orgs[0], nil +} + +func getOrgsFilteredQuery(startOrgName string, limit int, asc bool) sq.SelectBuilder { + fields := []string{"id", "data"} + + s := sb.Select(fields...).From("org as org") + if asc { + s = s.OrderBy("org.name asc") + } else { + s = s.OrderBy("org.name desc") + } + if startOrgName != "" { + if asc { + s = s.Where(sq.Gt{"org.name": startOrgName}) + } else { + s = s.Where(sq.Lt{"org.name": startOrgName}) + } + } + if limit > 0 { + s = s.Limit(uint64(limit)) + } + + return s +} + +func (r *ReadDB) GetOrgs(tx *db.Tx, startOrgName string, limit int, asc bool) ([]*types.Organization, error) { + var orgs []*types.Organization + + s := getOrgsFilteredQuery(startOrgName, limit, asc) + q, args, err := s.ToSql() + r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) + if err != nil { + return nil, errors.Wrap(err, "failed to build query") + } + + rows, err := tx.Query(q, args...) + if err != nil { + return nil, err + } + + orgs, _, err = scanOrgs(rows) + return orgs, err +} + +func fetchOrgs(tx *db.Tx, q string, args ...interface{}) ([]*types.Organization, []string, error) { + rows, err := tx.Query(q, args...) + if err != nil { + return nil, nil, err + } + defer rows.Close() + return scanOrgs(rows) +} + +func scanOrg(rows *sql.Rows, additionalFields ...interface{}) (*types.Organization, string, error) { + var id string + var data []byte + if err := rows.Scan(&id, &data); err != nil { + return nil, "", errors.Wrap(err, "failed to scan rows") + } + org := types.Organization{} + if len(data) > 0 { + if err := json.Unmarshal(data, &org); err != nil { + return nil, "", errors.Wrap(err, "failed to unmarshal org") + } + } + + return &org, id, nil +} + +func scanOrgs(rows *sql.Rows) ([]*types.Organization, []string, error) { + orgs := []*types.Organization{} + ids := []string{} + for rows.Next() { + p, id, err := scanOrg(rows) + if err != nil { + rows.Close() + return nil, nil, err + } + orgs = append(orgs, p) + ids = append(ids, id) + } + if err := rows.Err(); err != nil { + return nil, nil, err + } + return orgs, ids, nil +} diff --git a/internal/services/configstore/readdb/project.go b/internal/services/configstore/readdb/project.go index 57dab35..5ebe91d 100644 --- a/internal/services/configstore/readdb/project.go +++ b/internal/services/configstore/readdb/project.go @@ -28,7 +28,7 @@ import ( var ( projectSelect = sb.Select("id", "data").From("project") - projectInsert = sb.Insert("project").Columns("id", "name", "data") + projectInsert = sb.Insert("project").Columns("id", "name", "ownerid", "data") ) func (r *ReadDB) insertProject(tx *db.Tx, data []byte) error { @@ -40,7 +40,7 @@ func (r *ReadDB) insertProject(tx *db.Tx, data []byte) error { if err := r.deleteProject(tx, project.ID); err != nil { return err } - q, args, err := projectInsert.Values(project.ID, project.Name, data).ToSql() + q, args, err := projectInsert.Values(project.ID, project.Name, project.OwnerID, data).ToSql() if err != nil { return errors.Wrap(err, "failed to build query") } @@ -76,8 +76,8 @@ func (r *ReadDB) GetProject(tx *db.Tx, projectID string) (*types.Project, error) return projects[0], nil } -func (r *ReadDB) GetProjectByName(tx *db.Tx, name string) (*types.Project, error) { - q, args, err := projectSelect.Where(sq.Eq{"name": name}).ToSql() +func (r *ReadDB) GetOwnerProjectByName(tx *db.Tx, ownerid, name string) (*types.Project, error) { + q, args, err := projectSelect.Where(sq.Eq{"ownerid": ownerid, "name": name}).ToSql() r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) if err != nil { return nil, errors.Wrap(err, "failed to build query") @@ -96,7 +96,7 @@ func (r *ReadDB) GetProjectByName(tx *db.Tx, name string) (*types.Project, error return projects[0], nil } -func getProjectsFilteredQuery(startProjectName string, limit int, asc bool) sq.SelectBuilder { +func getProjectsFilteredQuery(ownerid, startProjectName string, limit int, asc bool) sq.SelectBuilder { fields := []string{"id", "data"} s := sb.Select(fields...).From("project as project") @@ -105,6 +105,9 @@ func getProjectsFilteredQuery(startProjectName string, limit int, asc bool) sq.S } else { s = s.OrderBy("project.name desc") } + if ownerid != "" { + s = s.Where(sq.Eq{"project.ownerid": ownerid}) + } if startProjectName != "" { if asc { s = s.Where(sq.Gt{"project.name": startProjectName}) @@ -118,26 +121,43 @@ func getProjectsFilteredQuery(startProjectName string, limit int, asc bool) sq.S return s } -func (r *ReadDB) GetProjects(startProjectName string, limit int, asc bool) ([]*types.Project, error) { + +func (r *ReadDB) GetOwnerProjects(tx *db.Tx, ownerid, startProjectName string, limit int, asc bool) ([]*types.Project, error) { var projects []*types.Project - s := getProjectsFilteredQuery(startProjectName, limit, asc) + s := getProjectsFilteredQuery(ownerid, startProjectName, limit, asc) q, args, err := s.ToSql() r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) if err != nil { return nil, errors.Wrap(err, "failed to build query") } - err = r.rdb.Do(func(tx *db.Tx) error { - rows, err := tx.Query(q, args...) - if err != nil { - return err - } + rows, err := tx.Query(q, args...) + if err != nil { + return nil, err + } - projects, _, err = scanProjects(rows) - return err - }) - return projects, errors.WithStack(err) + projects, _, err = scanProjects(rows) + return projects, err +} + +func (r *ReadDB) GetProjects(tx *db.Tx, startProjectName string, limit int, asc bool) ([]*types.Project, error) { + var projects []*types.Project + + s := getProjectsFilteredQuery("", startProjectName, limit, asc) + q, args, err := s.ToSql() + r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) + if err != nil { + return nil, errors.Wrap(err, "failed to build query") + } + + rows, err := tx.Query(q, args...) + if err != nil { + return nil, err + } + + projects, _, err = scanProjects(rows) + return projects, err } func fetchProjects(tx *db.Tx, q string, args ...interface{}) ([]*types.Project, []string, error) { diff --git a/internal/services/configstore/readdb/readdb.go b/internal/services/configstore/readdb/readdb.go index 2609a07..9ad4e04 100644 --- a/internal/services/configstore/readdb/readdb.go +++ b/internal/services/configstore/readdb/readdb.go @@ -627,6 +627,10 @@ func (r *ReadDB) applyAction(tx *db.Tx, action *wal.Action) error { if err := r.insertUser(tx, action.Data); err != nil { return err } + case common.ConfigTypeOrg: + if err := r.insertOrg(tx, action.Data); err != nil { + return err + } case common.ConfigTypeRemoteSource: if err := r.insertRemoteSource(tx, action.Data); err != nil { return err @@ -645,6 +649,11 @@ func (r *ReadDB) applyAction(tx *db.Tx, action *wal.Action) error { if err := r.deleteUser(tx, ID); err != nil { return err } + case common.ConfigTypeOrg: + r.log.Debugf("deleting org with id: %s", ID) + if err := r.deleteOrg(tx, ID); err != nil { + return err + } case common.ConfigTypeRemoteSource: r.log.Debugf("deleting remote source with id: %s", ID) if err := r.deleteRemoteSource(tx, ID); err != nil { diff --git a/internal/services/configstore/readdb/user.go b/internal/services/configstore/readdb/user.go index 3e958d3..fc8da9e 100644 --- a/internal/services/configstore/readdb/user.go +++ b/internal/services/configstore/readdb/user.go @@ -277,6 +277,7 @@ func getUsersFilteredQuery(startUserName string, limit int, asc bool) sq.SelectB return s } + func (r *ReadDB) GetUsers(tx *db.Tx, startUserName string, limit int, asc bool) ([]*types.User, error) { var users []*types.User diff --git a/internal/services/gateway/api/client.go b/internal/services/gateway/api/client.go index 4c438e5..4b91d01 100644 --- a/internal/services/gateway/api/client.go +++ b/internal/services/gateway/api/client.go @@ -23,6 +23,7 @@ import ( "io/ioutil" "net/http" "net/url" + "path" "strconv" "strings" @@ -118,7 +119,19 @@ func (c *Client) GetProject(ctx context.Context, projectID string) (*types.Proje return project, resp, err } -func (c *Client) GetProjects(ctx context.Context, start string, limit int, asc bool) (*GetProjectsResponse, *http.Response, error) { +func (c *Client) GetCurrentUserProjects(ctx context.Context, start string, limit int, asc bool) (*GetProjectsResponse, *http.Response, error) { + return c.getProjects(ctx, "user", "", start, limit, asc) +} + +func (c *Client) GetUserProjects(ctx context.Context, username, start string, limit int, asc bool) (*GetProjectsResponse, *http.Response, error) { + return c.getProjects(ctx, "user", username, start, limit, asc) +} + +func (c *Client) GetOrgProjects(ctx context.Context, orgname, start string, limit int, asc bool) (*GetProjectsResponse, *http.Response, error) { + return c.getProjects(ctx, "org", orgname, start, limit, asc) +} + +func (c *Client) getProjects(ctx context.Context, ownertype, ownername, start string, limit int, asc bool) (*GetProjectsResponse, *http.Response, error) { q := url.Values{} if start != "" { q.Add("start", start) @@ -131,23 +144,47 @@ func (c *Client) GetProjects(ctx context.Context, start string, limit int, asc b } projects := new(GetProjectsResponse) - resp, err := c.getParsedResponse(ctx, "GET", "/projects", q, jsonContent, nil, &projects) + resp, err := c.getParsedResponse(ctx, "GET", path.Join("/", ownertype, ownername, "projects"), q, jsonContent, nil, &projects) return projects, resp, err } -func (c *Client) CreateProject(ctx context.Context, req *CreateProjectRequest) (*types.Project, *http.Response, error) { +func (c *Client) CreateCurrentUserProject(ctx context.Context, req *CreateProjectRequest) (*types.Project, *http.Response, error) { + return c.createProject(ctx, "user", "", req) +} + +func (c *Client) CreateUserProject(ctx context.Context, username string, req *CreateProjectRequest) (*types.Project, *http.Response, error) { + return c.createProject(ctx, "user", username, req) +} + +func (c *Client) CreateOrgProject(ctx context.Context, orgname string, req *CreateProjectRequest) (*types.Project, *http.Response, error) { + return c.createProject(ctx, "org", orgname, req) +} + +func (c *Client) createProject(ctx context.Context, ownertype, ownername string, req *CreateProjectRequest) (*types.Project, *http.Response, error) { reqj, err := json.Marshal(req) if err != nil { return nil, nil, err } project := new(types.Project) - resp, err := c.getParsedResponse(ctx, "PUT", "/projects", nil, jsonContent, bytes.NewReader(reqj), project) + resp, err := c.getParsedResponse(ctx, "PUT", path.Join("/", ownertype, ownername, "projects"), nil, jsonContent, bytes.NewReader(reqj), project) return project, resp, err } -func (c *Client) DeleteProject(ctx context.Context, projectName string) (*http.Response, error) { - return c.getResponse(ctx, "DELETE", fmt.Sprintf("/projects/%s", projectName), nil, jsonContent, nil) +func (c *Client) DeleteCurrentUserProject(ctx context.Context, projectName string) (*http.Response, error) { + return c.deleteProject(ctx, "user", "", projectName) +} + +func (c *Client) DeleteUserProject(ctx context.Context, username, projectName string) (*http.Response, error) { + return c.deleteProject(ctx, "user", username, projectName) +} + +func (c *Client) DeleteOrgProject(ctx context.Context, orgname, projectName string) (*http.Response, error) { + return c.deleteProject(ctx, "org", orgname, projectName) +} + +func (c *Client) deleteProject(ctx context.Context, ownertype, ownername, projectName string) (*http.Response, error) { + return c.getResponse(ctx, "DELETE", path.Join("/projects", ownertype, ownername, projectName), nil, jsonContent, nil) } func (c *Client) ReconfigProject(ctx context.Context, projectName string) (*http.Response, error) { @@ -287,3 +324,18 @@ func (c *Client) CreateRemoteSource(ctx context.Context, req *CreateRemoteSource func (c *Client) DeleteRemoteSource(ctx context.Context, name string) (*http.Response, error) { return c.getResponse(ctx, "DELETE", fmt.Sprintf("/remotesources/%s", name), nil, jsonContent, nil) } + +func (c *Client) CreateOrg(ctx context.Context, req *CreateOrgRequest) (*OrgResponse, *http.Response, error) { + reqj, err := json.Marshal(req) + if err != nil { + return nil, nil, err + } + + org := new(OrgResponse) + resp, err := c.getParsedResponse(ctx, "PUT", "/orgs", nil, jsonContent, bytes.NewReader(reqj), org) + return org, resp, err +} + +func (c *Client) DeleteOrg(ctx context.Context, orgName string) (*http.Response, error) { + return c.getResponse(ctx, "DELETE", fmt.Sprintf("/orgs/%s", orgName), nil, jsonContent, nil) +} diff --git a/internal/services/gateway/api/org.go b/internal/services/gateway/api/org.go new file mode 100644 index 0000000..4122496 --- /dev/null +++ b/internal/services/gateway/api/org.go @@ -0,0 +1,289 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + + csapi "github.com/sorintlab/agola/internal/services/configstore/api" + "github.com/sorintlab/agola/internal/services/types" + "github.com/sorintlab/agola/internal/util" + "go.uber.org/zap" + + "github.com/gorilla/mux" + "github.com/pkg/errors" +) + +type CreateOrgRequest struct { + Name string `json:"name"` +} + +type CreateOrgHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewCreateOrgHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CreateOrgHandler { + return &CreateOrgHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *CreateOrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var req CreateOrgRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + org, err := h.createOrg(ctx, &req) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := json.NewEncoder(w).Encode(org); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + +} + +func (h *CreateOrgHandler) createOrg(ctx context.Context, req *CreateOrgRequest) (*OrgResponse, error) { + if !util.ValidateName(req.Name) { + return nil, errors.Errorf("invalid org name %q", req.Name) + } + + u := &types.Organization{ + Name: req.Name, + } + + h.log.Infof("creating org") + u, _, err := h.configstoreClient.CreateOrg(ctx, u) + if err != nil { + return nil, errors.Wrapf(err, "failed to create org") + } + h.log.Infof("org %s created, ID: %s", u.Name, u.ID) + + res := createOrgResponse(u) + return res, nil +} + +type DeleteOrgHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewDeleteOrgHandler(logger *zap.Logger, configstoreClient *csapi.Client) *DeleteOrgHandler { + return &DeleteOrgHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *DeleteOrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + orgName := vars["orgname"] + + resp, err := h.configstoreClient.DeleteOrg(ctx, orgName) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type CurrentOrgHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewCurrentOrgHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CurrentOrgHandler { + return &CurrentOrgHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *CurrentOrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + orgIDVal := ctx.Value("orgid") + if orgIDVal == nil { + http.Error(w, "", http.StatusBadRequest) + return + } + orgID := orgIDVal.(string) + + org, resp, err := h.configstoreClient.GetOrg(ctx, orgID) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := createOrgResponse(org) + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type OrgHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewOrgHandler(logger *zap.Logger, configstoreClient *csapi.Client) *OrgHandler { + return &OrgHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *OrgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + orgID := vars["orgid"] + + org, resp, err := h.configstoreClient.GetOrg(ctx, orgID) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := createOrgResponse(org) + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type OrgByNameHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewOrgByNameHandler(logger *zap.Logger, configstoreClient *csapi.Client) *OrgByNameHandler { + return &OrgByNameHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *OrgByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + orgName := vars["orgname"] + + org, resp, err := h.configstoreClient.GetOrgByName(ctx, orgName) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := createOrgResponse(org) + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type OrgsResponse struct { + Orgs []*OrgResponse `json:"orgs"` +} + +type OrgResponse struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func createOrgResponse(r *types.Organization) *OrgResponse { + org := &OrgResponse{ + ID: r.ID, + Name: r.Name, + } + return org +} + +type OrgsHandler struct { + log *zap.SugaredLogger + configstoreClient *csapi.Client +} + +func NewOrgsHandler(logger *zap.Logger, configstoreClient *csapi.Client) *OrgsHandler { + return &OrgsHandler{log: logger.Sugar(), configstoreClient: configstoreClient} +} + +func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + query := r.URL.Query() + + limitS := query.Get("limit") + limit := DefaultRunsLimit + if limitS != "" { + var err error + limit, err = strconv.Atoi(limitS) + if err != nil { + http.Error(w, "", http.StatusBadRequest) + return + } + } + if limit < 0 { + http.Error(w, "limit must be greater or equal than 0", http.StatusBadRequest) + return + } + if limit > MaxRunsLimit { + limit = MaxRunsLimit + } + asc := false + if _, ok := query["asc"]; ok { + asc = true + } + + start := query.Get("start") + + csorgs, resp, err := h.configstoreClient.GetOrgs(ctx, start, limit, asc) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + orgs := make([]*OrgResponse, len(csorgs)) + for i, p := range csorgs { + orgs[i] = createOrgResponse(p) + } + orgsResponse := &OrgsResponse{ + Orgs: orgs, + } + + if err := json.NewEncoder(w).Encode(orgsResponse); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/internal/services/gateway/api/project.go b/internal/services/gateway/api/project.go index afde542..1dff159 100644 --- a/internal/services/gateway/api/project.go +++ b/internal/services/gateway/api/project.go @@ -15,10 +15,13 @@ package api import ( + "context" "encoding/json" + "fmt" "net/http" "strconv" + "github.com/pkg/errors" csapi "github.com/sorintlab/agola/internal/services/configstore/api" "github.com/sorintlab/agola/internal/services/gateway/command" "github.com/sorintlab/agola/internal/services/types" @@ -47,6 +50,8 @@ func NewCreateProjectHandler(logger *zap.Logger, ch *command.CommandHandler, con func (h *CreateProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + vars := mux.Vars(r) + orgname := vars["orgname"] var req CreateProjectRequest d := json.NewDecoder(r.Body) @@ -57,19 +62,32 @@ func (h *CreateProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) ctxUserID := ctx.Value("userid") if ctxUserID == nil { - http.Error(w, "no userid specified", http.StatusBadRequest) + http.Error(w, "no authenticated user", http.StatusBadRequest) return } userID := ctxUserID.(string) h.log.Infof("userID: %q", userID) - project, err := h.ch.CreateProject(ctx, &command.CreateProjectRequest{ + creq := &command.CreateProjectRequest{ Name: req.Name, RepoURL: req.RepoURL, RemoteSourceName: req.RemoteSourceName, UserID: userID, SkipSSHHostKeyCheck: req.SkipSSHHostKeyCheck, - }) + } + + ownerID, code, userErr, err := getOwnerID(ctx, h.configstoreClient, "", orgname, true) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, userErr, code) + return + } + if orgname != "" { + creq.OwnerType = types.OwnerTypeOrganization + creq.OwnerID = ownerID + } + + project, err := h.ch.CreateProject(ctx, creq) if err != nil { h.log.Errorf("err: %+v", err) http.Error(w, err.Error(), http.StatusBadRequest) @@ -81,7 +99,6 @@ func (h *CreateProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) http.Error(w, err.Error(), http.StatusInternalServerError) return } - } type ProjectReconfigHandler struct { @@ -99,8 +116,17 @@ func (h *ProjectReconfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques ctx := r.Context() vars := mux.Vars(r) projectName := vars["projectname"] + username := vars["username"] + orgname := vars["orgname"] - if err := h.ch.ReconfigProject(ctx, projectName); err != nil { + ownerID, code, userErr, err := getOwnerID(ctx, h.configstoreClient, username, orgname, false) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, userErr, code) + return + } + + if err := h.ch.ReconfigProject(ctx, ownerID, projectName); err != nil { h.log.Errorf("err: %+v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -120,8 +146,28 @@ func (h *DeleteProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) ctx := r.Context() vars := mux.Vars(r) projectName := vars["projectname"] + username := vars["username"] + orgname := vars["orgname"] - resp, err := h.configstoreClient.DeleteProject(ctx, projectName) + ownerID, code, userErr, err := getOwnerID(ctx, h.configstoreClient, username, orgname, true) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, userErr, code) + return + } + + project, resp, err := h.configstoreClient.GetProjectByName(ctx, ownerID, projectName) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, fmt.Sprintf("project with name %q doesn't exist", projectName), http.StatusNotFound) + return + } + h.log.Errorf("err: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp, err = h.configstoreClient.DeleteProject(ctx, project.ID) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { http.Error(w, err.Error(), http.StatusNotFound) @@ -179,8 +225,17 @@ func (h *ProjectByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) ctx := r.Context() vars := mux.Vars(r) projectName := vars["projectname"] + username := vars["username"] + orgname := vars["orgname"] - project, resp, err := h.configstoreClient.GetProjectByName(ctx, projectName) + ownerID, code, userErr, err := getOwnerID(ctx, h.configstoreClient, username, orgname, false) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, userErr, code) + return + } + + project, resp, err := h.configstoreClient.GetProjectByName(ctx, ownerID, projectName) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { http.Error(w, err.Error(), http.StatusNotFound) @@ -228,9 +283,19 @@ func NewProjectsHandler(logger *zap.Logger, configstoreClient *csapi.Client) *Pr func (h *ProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - + vars := mux.Vars(r) query := r.URL.Query() + username := vars["username"] + orgname := vars["orgname"] + + ownerID, code, userErr, err := getOwnerID(ctx, h.configstoreClient, username, orgname, true) + if err != nil { + h.log.Errorf("err: %+v", err) + http.Error(w, userErr, code) + return + } + limitS := query.Get("limit") limit := DefaultRunsLimit if limitS != "" { @@ -255,7 +320,7 @@ func (h *ProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := query.Get("start") - csprojects, resp, err := h.configstoreClient.GetProjects(ctx, start, limit, asc) + csprojects, resp, err := h.configstoreClient.GetOwnerProjects(ctx, ownerID, start, limit, asc) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { http.Error(w, err.Error(), http.StatusNotFound) @@ -280,3 +345,44 @@ func (h *ProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } + +func getOwnerID(ctx context.Context, configstoreClient *csapi.Client, username, orgname string, useAuthUser bool) (string, int, string, error) { + var ownerID string + switch { + case username != "": + user, resp, err := configstoreClient.GetUserByName(ctx, username) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + err = errors.Errorf("user %q doens't exist", username) + return "", http.StatusNotFound, err.Error(), err + } + return "", http.StatusInternalServerError, "", err + } + ownerID = user.ID + case orgname != "": + org, resp, err := configstoreClient.GetOrgByName(ctx, orgname) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + err = errors.Errorf("organization %q doens't exist", orgname) + return "", http.StatusNotFound, err.Error(), err + } + return "", http.StatusInternalServerError, "", err + } + ownerID = org.ID + default: + if useAuthUser { + // use the current authenticated user + ctxUserID := ctx.Value("userid") + if ctxUserID == nil { + err := errors.New("no authenticated user") + return "", http.StatusBadRequest, err.Error(), err + } + ownerID = ctxUserID.(string) + } else { + err := errors.New("no user or org name specified") + return "", http.StatusBadRequest, err.Error(), err + } + } + + return ownerID, 0, "", nil +} diff --git a/internal/services/gateway/command/project.go b/internal/services/gateway/command/project.go index eb26b27..1d8649d 100644 --- a/internal/services/gateway/command/project.go +++ b/internal/services/gateway/command/project.go @@ -33,6 +33,8 @@ type CreateProjectRequest struct { RemoteSourceName string RepoURL string UserID string + OwnerType types.OwnerType + OwnerID string SkipSSHHostKeyCheck bool } @@ -86,6 +88,8 @@ func (c *CommandHandler) CreateProject(ctx context.Context, req *CreateProjectRe p := &types.Project{ Name: req.Name, + OwnerType: types.OwnerTypeUser, + OwnerID: user.ID, LinkedAccountID: la.ID, Path: fmt.Sprintf("%s/%s", repoOwner, repoName), CloneURL: cloneURL, @@ -93,6 +97,14 @@ func (c *CommandHandler) CreateProject(ctx context.Context, req *CreateProjectRe SSHPrivateKey: string(privateKey), } + if req.OwnerType == types.OwnerTypeOrganization { + if req.OwnerID == "" { + return nil, errors.Errorf("ownerid must be specified when adding a project outside the current user") + } + p.OwnerType = req.OwnerType + p.OwnerID = req.OwnerID + } + c.log.Infof("creating project") p, _, err = c.configstoreClient.CreateProject(ctx, p) if err != nil { @@ -141,8 +153,8 @@ func (c *CommandHandler) SetupProject(ctx context.Context, rs *types.RemoteSourc return nil } -func (c *CommandHandler) ReconfigProject(ctx context.Context, projectName string) error { - p, _, err := c.configstoreClient.GetProjectByName(ctx, projectName) +func (c *CommandHandler) ReconfigProject(ctx context.Context, ownerID, projectName string) error { + p, _, err := c.configstoreClient.GetProjectByName(ctx, ownerID, projectName) if err != nil { return err } diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index 15424d3..e904885 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -165,6 +165,12 @@ func (g *Gateway) Run(ctx context.Context) error { createRemoteSourceHandler := api.NewCreateRemoteSourceHandler(logger, g.configstoreClient) remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, g.configstoreClient) + orgHandler := api.NewOrgHandler(logger, g.configstoreClient) + orgByNameHandler := api.NewOrgByNameHandler(logger, g.configstoreClient) + orgsHandler := api.NewOrgsHandler(logger, g.configstoreClient) + createOrgHandler := api.NewCreateOrgHandler(logger, g.configstoreClient) + deleteOrgHandler := api.NewDeleteOrgHandler(logger, g.configstoreClient) + runHandler := api.NewRunHandler(logger, g.runserviceClient) runsHandler := api.NewRunsHandler(logger, g.runserviceClient) runtaskHandler := api.NewRuntaskHandler(logger, g.runserviceClient) @@ -188,11 +194,18 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/logs", logsHandler).Methods("GET") apirouter.Handle("/project/{projectid}", authForcedHandler(projectHandler)).Methods("GET") - apirouter.Handle("/projects", authForcedHandler(projectsHandler)).Methods("GET") - apirouter.Handle("/projects", authForcedHandler(createProjectHandler)).Methods("PUT") - apirouter.Handle("/projects/{projectname}", authForcedHandler(projectByNameHandler)).Methods("GET") - apirouter.Handle("/projects/{projectname}", authForcedHandler(deleteProjectHandler)).Methods("DELETE") - apirouter.Handle("/projects/{projectname}/reconfig", authForcedHandler(projectReconfigHandler)).Methods("POST") + apirouter.Handle("/user/projects", authForcedHandler(projectsHandler)).Methods("GET") + apirouter.Handle("/user/{username}/projects", authForcedHandler(projectsHandler)).Methods("GET") + apirouter.Handle("/org/{orgname}/projects", authForcedHandler(projectsHandler)).Methods("GET") + apirouter.Handle("/user/projects", authForcedHandler(createProjectHandler)).Methods("PUT") + apirouter.Handle("/org/{orgname}/projects", authForcedHandler(createProjectHandler)).Methods("PUT") + apirouter.Handle("/projects/user/{username}/{projectname}", authForcedHandler(projectByNameHandler)).Methods("GET") + apirouter.Handle("/projects/org/{orgname}/{projectname}", authForcedHandler(projectByNameHandler)).Methods("GET") + apirouter.Handle("/projects/user/{projectname}", authForcedHandler(deleteProjectHandler)).Methods("DELETE") + apirouter.Handle("/projects/user/{username}/{projectname}", authForcedHandler(deleteProjectHandler)).Methods("DELETE") + apirouter.Handle("/projects/org/{orgname}/{projectname}", authForcedHandler(deleteProjectHandler)).Methods("DELETE") + apirouter.Handle("/projects/user/{username}/{projectname}/reconfig", authForcedHandler(projectReconfigHandler)).Methods("POST") + apirouter.Handle("/projects/org/{orgname}/{projectname}/reconfig", authForcedHandler(projectReconfigHandler)).Methods("POST") apirouter.Handle("/user", authForcedHandler(currentUserHandler)).Methods("GET") apirouter.Handle("/user/{userid}", authForcedHandler(userHandler)).Methods("GET") @@ -209,6 +222,12 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/remotesources", authForcedHandler(createRemoteSourceHandler)).Methods("PUT") apirouter.Handle("/remotesources", authOptionalHandler(remoteSourcesHandler)).Methods("GET") + apirouter.Handle("/org/{orgid}", authForcedHandler(orgHandler)).Methods("GET") + apirouter.Handle("/orgs", authForcedHandler(orgsHandler)).Methods("GET") + apirouter.Handle("/orgs", authForcedHandler(createOrgHandler)).Methods("PUT") + apirouter.Handle("/orgs/{orgname}", authForcedHandler(orgByNameHandler)).Methods("GET") + apirouter.Handle("/orgs/{orgname}", authForcedHandler(deleteOrgHandler)).Methods("DELETE") + apirouter.Handle("/run/{runid}", authForcedHandler(runHandler)).Methods("GET") apirouter.Handle("/run/{runid}/task/{taskid}", authForcedHandler(runtaskHandler)).Methods("GET") apirouter.Handle("/runs", authForcedHandler(runsHandler)).Methods("GET")