*: add organizations

This commit is contained in:
Simone Gotti 2019-02-28 15:52:35 +01:00
parent e6595b2dba
commit 41002efbff
26 changed files with 1663 additions and 142 deletions

28
cmd/agola/cmd/org.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -35,6 +35,7 @@ var cmdProjectDelete = &cobra.Command{
type projectDeleteOptions struct {
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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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"]

View File

@ -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"]

View File

@ -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
}

View File

@ -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:

View File

@ -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()

View File

@ -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))
}
})
}

View File

@ -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))",

View File

@ -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
}

View File

@ -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
return nil, err
}
projects, _, err = scanProjects(rows)
return err
})
return projects, errors.WithStack(err)
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) {

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")