gateway: initial implementation

This commit is contained in:
Simone Gotti 2019-02-21 17:58:25 +01:00
parent 4c0edd6374
commit 021a0465ce
44 changed files with 4910 additions and 52 deletions

View File

@ -30,6 +30,8 @@ var level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
var logger = slog.New(level)
var log = logger.Sugar()
var token string
var cmdAgola = &cobra.Command{
Use: "agola",
Short: "agola",
@ -68,6 +70,7 @@ func init() {
flags := cmdAgola.PersistentFlags()
flags.StringVarP(&agolaOpts.gatewayURL, "gateway-url", "u", gatewayURL, "agola gateway exposed url")
flags.StringVar(&token, "token", token, "api token")
flags.BoolVarP(&agolaOpts.debug, "debug", "d", false, "debug")
}

28
cmd/agola/cmd/project.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 cmdProject = &cobra.Command{
Use: "project",
Short: "project",
}
func init() {
cmdAgola.AddCommand(cmdProject)
}

View File

@ -0,0 +1,78 @@
// 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 cmdProjectCreate = &cobra.Command{
Use: "create",
Short: "create a project",
Run: func(cmd *cobra.Command, args []string) {
if err := projectCreate(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type projectCreateOptions struct {
name string
repoURL string
remoteSourceName string
skipSSHHostKeyCheck bool
}
var projectCreateOpts projectCreateOptions
func init() {
flags := cmdProjectCreate.Flags()
flags.StringVarP(&projectCreateOpts.name, "name", "n", "", "project name")
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")
cmdProjectCreate.MarkFlagRequired("name")
cmdProjectCreate.MarkFlagRequired("repo-url")
cmdProjectCreate.MarkFlagRequired("remote-source")
cmdProject.AddCommand(cmdProjectCreate)
}
func projectCreate(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
req := &api.CreateProjectRequest{
Name: projectCreateOpts.name,
RepoURL: projectCreateOpts.repoURL,
RemoteSourceName: projectCreateOpts.remoteSourceName,
SkipSSHHostKeyCheck: projectCreateOpts.skipSSHHostKeyCheck,
}
log.Infof("creating project")
project, _, err := gwclient.CreateProject(context.TODO(), req)
if err != nil {
return errors.Wrapf(err, "failed to create project")
}
log.Infof("project %s created, ID: %s", project.Name, project.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 cmdProjectDelete = &cobra.Command{
Use: "delete",
Short: "delete a project",
Run: func(cmd *cobra.Command, args []string) {
if err := projectDelete(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type projectDeleteOptions struct {
name string
}
var projectDeleteOpts projectDeleteOptions
func init() {
flags := cmdProjectDelete.Flags()
flags.StringVarP(&projectDeleteOpts.name, "name", "n", "", "project name")
cmdProjectDelete.MarkFlagRequired("name")
cmdProject.AddCommand(cmdProjectDelete)
}
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 {
return errors.Wrapf(err, "failed to delete project")
}
return nil
}

View File

@ -0,0 +1,68 @@
// 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"
"fmt"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdProjectList = &cobra.Command{
Use: "list",
Run: func(cmd *cobra.Command, args []string) {
if err := projectList(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
Short: "list",
}
type projectListOptions struct {
limit int
start string
}
var projectListOpts projectListOptions
func init() {
flags := cmdProjectList.PersistentFlags()
flags.IntVar(&projectListOpts.limit, "limit", 10, "max number of runs to show")
flags.StringVar(&projectListOpts.start, "start", "", "starting project name (excluded) to fetch")
cmdProject.AddCommand(cmdProjectList)
}
func printProjects(projectsResponse *api.GetProjectsResponse) {
for _, project := range projectsResponse.Projects {
fmt.Printf("%s: Name: %s\n", project.ID, project.Name)
}
}
func projectList(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
projectsResponse, _, err := gwclient.GetProjects(context.TODO(), projectListOpts.start, projectListOpts.limit, false)
if err != nil {
return err
}
printProjects(projectsResponse)
return nil
}

View File

@ -0,0 +1,62 @@
// 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 cmdProjectReconfig = &cobra.Command{
Use: "reconfig",
Short: "reconfigures a project remote (reinstalls ssh deploy key and webhooks",
Run: func(cmd *cobra.Command, args []string) {
if err := projectReconfig(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type projectReconfigOptions struct {
name string
}
var projectReconfigOpts projectReconfigOptions
func init() {
flags := cmdProjectReconfig.Flags()
flags.StringVarP(&projectReconfigOpts.name, "name", "n", "", "project name")
cmdProjectReconfig.MarkFlagRequired("name")
cmdProject.AddCommand(cmdProjectReconfig)
}
func projectReconfig(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
log.Infof("reconfiguring remote project")
if _, err := gwclient.ReconfigProject(context.TODO(), projectReconfigOpts.name); err != nil {
return errors.Wrapf(err, "failed to reconfigure remote project")
}
log.Infof("project reconfigured")
return nil
}

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 cmdRemoteSource = &cobra.Command{
Use: "remotesource",
Short: "remotesource",
}
func init() {
cmdAgola.AddCommand(cmdRemoteSource)
}

View File

@ -0,0 +1,85 @@
// 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 cmdRemoteSourceCreate = &cobra.Command{
Use: "create",
Short: "create a remotesource",
Run: func(cmd *cobra.Command, args []string) {
if err := remoteSourceCreate(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type remoteSourceCreateOptions struct {
name string
rsType string
authType string
apiURL string
oauth2ClientID string
oauth2ClientSecret string
}
var remoteSourceCreateOpts remoteSourceCreateOptions
func init() {
flags := cmdRemoteSourceCreate.Flags()
flags.StringVarP(&remoteSourceCreateOpts.name, "name", "n", "", "remotesource name")
flags.StringVar(&remoteSourceCreateOpts.rsType, "type", "", "remotesource type")
flags.StringVar(&remoteSourceCreateOpts.authType, "auth-type", "", "remote source auth type")
flags.StringVar(&remoteSourceCreateOpts.apiURL, "api-url", "", "remotesource api url")
flags.StringVar(&remoteSourceCreateOpts.oauth2ClientID, "clientid", "", "remotesource oauth2 client id")
flags.StringVar(&remoteSourceCreateOpts.oauth2ClientSecret, "secret", "", "remotesource oauth2 secret")
cmdRemoteSourceCreate.MarkFlagRequired("name")
cmdRemoteSourceCreate.MarkFlagRequired("type")
cmdRemoteSourceCreate.MarkFlagRequired("auth-type")
cmdRemoteSourceCreate.MarkFlagRequired("api-url")
cmdRemoteSource.AddCommand(cmdRemoteSourceCreate)
}
func remoteSourceCreate(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
req := &api.CreateRemoteSourceRequest{
Name: remoteSourceCreateOpts.name,
Type: remoteSourceCreateOpts.rsType,
AuthType: remoteSourceCreateOpts.authType,
APIURL: remoteSourceCreateOpts.apiURL,
Oauth2ClientID: remoteSourceCreateOpts.oauth2ClientID,
Oauth2ClientSecret: remoteSourceCreateOpts.oauth2ClientSecret,
}
log.Infof("creating remotesource")
remoteSource, _, err := gwclient.CreateRemoteSource(context.TODO(), req)
if err != nil {
return errors.Wrapf(err, "failed to create remotesource")
}
log.Infof("remotesource %s created, ID: %s", remoteSource.Name, remoteSource.ID)
return nil
}

View File

@ -0,0 +1,68 @@
// 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"
"fmt"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdRemoteSourceList = &cobra.Command{
Use: "list",
Run: func(cmd *cobra.Command, args []string) {
if err := remoteSourceList(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
Short: "list",
}
type remoteSourceListOptions struct {
limit int
start string
}
var remoteSourceListOpts remoteSourceListOptions
func init() {
flags := cmdRemoteSourceList.PersistentFlags()
flags.IntVar(&remoteSourceListOpts.limit, "limit", 10, "max number of runs to show")
flags.StringVar(&remoteSourceListOpts.start, "start", "", "starting user name (excluded) to fetch")
cmdRemoteSource.AddCommand(cmdRemoteSourceList)
}
func printRemoteSources(rssResponse *api.RemoteSourcesResponse) {
for _, rs := range rssResponse.RemoteSources {
fmt.Printf("%s: Name: %s\n", rs.ID, rs.Name)
}
}
func remoteSourceList(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
rssResponse, _, err := gwclient.GetRemoteSources(context.TODO(), remoteSourceListOpts.start, remoteSourceListOpts.limit, false)
if err != nil {
return err
}
printRemoteSources(rssResponse)
return nil
}

28
cmd/agola/cmd/run.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 cmdRun = &cobra.Command{
Use: "run",
Short: "run",
}
func init() {
cmdAgola.AddCommand(cmdRun)
}

85
cmd/agola/cmd/runlist.go Normal file
View File

@ -0,0 +1,85 @@
// 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"
"fmt"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdRunList = &cobra.Command{
Use: "list",
Run: func(cmd *cobra.Command, args []string) {
if err := runList(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
Short: "list",
}
type runListOptions struct {
statusFilter []string
labelFilter []string
limit int
start string
}
var runListOpts runListOptions
func init() {
flags := cmdRunList.PersistentFlags()
flags.StringSliceVarP(&runListOpts.statusFilter, "status", "s", nil, "filter runs matching the provided status. This option can be repeated multiple times")
flags.StringArrayVarP(&runListOpts.labelFilter, "label", "l", nil, "filter runs matching the provided label. This option can be repeated multiple times, in this case only runs matching all the labels will be returned")
flags.IntVar(&runListOpts.limit, "limit", 10, "max number of runs to show")
flags.StringVar(&runListOpts.start, "start", "", "starting run id (excluded) to fetch")
cmdRun.AddCommand(cmdRunList)
}
func printRuns(runs []*api.RunResponse) {
for _, run := range runs {
fmt.Printf("%s: Phase: %s, Result: %s\n", run.ID, run.Phase, run.Result)
for _, task := range run.Tasks {
fmt.Printf("\tTaskName: %s, Status: %s\n", task.Name, task.Status)
}
}
}
func runList(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
runsResp, _, err := gwclient.GetRuns(context.TODO(), runListOpts.statusFilter, runListOpts.labelFilter, []string{}, runListOpts.start, runListOpts.limit, false)
if err != nil {
return err
}
runs := make([]*api.RunResponse, len(runsResp.Runs))
for i, runsResponse := range runsResp.Runs {
run, _, err := gwclient.GetRun(context.TODO(), runsResponse.ID)
if err != nil {
return err
}
runs[i] = run
}
printRuns(runs)
return nil
}

View File

@ -21,6 +21,7 @@ import (
"github.com/sorintlab/agola/cmd"
"github.com/sorintlab/agola/internal/services/config"
"github.com/sorintlab/agola/internal/services/configstore"
"github.com/sorintlab/agola/internal/services/gateway"
"github.com/sorintlab/agola/internal/services/runservice/executor"
rsscheduler "github.com/sorintlab/agola/internal/services/runservice/scheduler"
"github.com/sorintlab/agola/internal/services/scheduler"
@ -127,11 +128,17 @@ func serve(cmd *cobra.Command, args []string) error {
return errors.Wrapf(err, "failed to start scheduler")
}
gateway, err := gateway.NewGateway(&c.Gateway)
if err != nil {
return errors.Wrapf(err, "failed to start gateway")
}
errCh := make(chan error)
go func() { errCh <- rsex1.Run(ctx) }()
go func() { errCh <- rssched1.Run(ctx) }()
go func() { errCh <- cs.Run(ctx) }()
go func() { errCh <- gateway.Run(ctx) }()
go func() { errCh <- sched1.Run(ctx) }()
return <-errCh

28
cmd/agola/cmd/user.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 cmdUser = &cobra.Command{
Use: "user",
Short: "user",
}
func init() {
cmdAgola.AddCommand(cmdUser)
}

View File

@ -0,0 +1,69 @@
// 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 cmdUserCreate = &cobra.Command{
Use: "create",
Short: "create a user",
Run: func(cmd *cobra.Command, args []string) {
if err := userCreate(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type userCreateOptions struct {
username string
}
var userCreateOpts userCreateOptions
func init() {
flags := cmdUserCreate.Flags()
flags.StringVarP(&userCreateOpts.username, "username", "n", "", "user name")
cmdUserCreate.MarkFlagRequired("username")
cmdUserCreate.MarkFlagRequired("repo-url")
cmdUserCreate.MarkFlagRequired("token")
cmdUser.AddCommand(cmdUserCreate)
}
func userCreate(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
req := &api.CreateUserRequest{
UserName: userCreateOpts.username,
}
log.Infof("creating user")
user, _, err := gwclient.CreateUser(context.TODO(), req)
if err != nil {
return errors.Wrapf(err, "failed to create user")
}
log.Infof("user %q created, ID: %q", user.UserName, user.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 cmdUserDelete = &cobra.Command{
Use: "delete",
Short: "delete a user",
Run: func(cmd *cobra.Command, args []string) {
if err := userDelete(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type userDeleteOptions struct {
username string
}
var userDeleteOpts userDeleteOptions
func init() {
flags := cmdUserDelete.Flags()
flags.StringVarP(&userDeleteOpts.username, "username", "n", "", "user name")
cmdUserDelete.MarkFlagRequired("username")
cmdUser.AddCommand(cmdUserDelete)
}
func userDelete(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
log.Infof("deleting user %q", userDeleteOpts.username)
if _, err := gwclient.DeleteUser(context.TODO(), userDeleteOpts.username); err != nil {
return errors.Wrapf(err, "failed to delete user")
}
return nil
}

28
cmd/agola/cmd/userla.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 cmdUserLA = &cobra.Command{
Use: "linkedaccount",
Short: "linkedaccount",
}
func init() {
cmdUser.AddCommand(cmdUserLA)
}

View File

@ -0,0 +1,98 @@
// 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 (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdUserLACreate = &cobra.Command{
Use: "create",
Short: "create a user linkedaccount",
Run: func(cmd *cobra.Command, args []string) {
if err := userLACreate(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type userLACreateOptions struct {
username string
remoteSourceName string
remoteSourceLoginName string
remoteSourceLoginPassword string
}
var userLACreateOpts userLACreateOptions
func init() {
flags := cmdUserLACreate.Flags()
flags.StringVarP(&userLACreateOpts.username, "username", "n", "", "user name")
flags.StringVarP(&userLACreateOpts.remoteSourceName, "remote-source", "r", "", "remote source name")
flags.StringVar(&userLACreateOpts.remoteSourceLoginName, "remote-name", "", "remote source login name")
flags.StringVar(&userLACreateOpts.remoteSourceLoginPassword, "remote-password", "", "remote source password")
cmdUserLACreate.MarkFlagRequired("username")
cmdUserLACreate.MarkFlagRequired("remote-source")
cmdUserLA.AddCommand(cmdUserLACreate)
}
func userLACreate(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
req := &api.CreateUserLARequest{
RemoteSourceName: userLACreateOpts.remoteSourceName,
RemoteSourceLoginName: userLACreateOpts.remoteSourceLoginName,
RemoteSourceLoginPassword: userLACreateOpts.remoteSourceLoginPassword,
}
log.Infof("creating linked account for user %q", userLACreateOpts.username)
resp, _, err := gwclient.CreateUserLA(context.TODO(), userLACreateOpts.username, req)
if err != nil {
return errors.Wrapf(err, "failed to create linked account")
}
if resp.Oauth2Redirect != "" {
log.Infof("visit %s", resp.Oauth2Redirect)
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter code: ")
code, _ := reader.ReadString('\n')
code = strings.TrimSpace(code)
log.Infof("code: %s", code)
req := &api.CreateUserLARequest{
RemoteSourceName: userLACreateOpts.remoteSourceName,
}
resp, _, err = gwclient.CreateUserLA(context.TODO(), userLACreateOpts.username, req)
if err != nil {
return errors.Wrapf(err, "failed to create linked account")
}
}
log.Infof("linked account for user %q created, ID: %s", userLACreateOpts.username, resp.LinkedAccount.ID)
return nil
}

View File

@ -0,0 +1,70 @@
// 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 cmdUserLADelete = &cobra.Command{
Use: "delete",
Short: "delete a user linkedaccount",
Run: func(cmd *cobra.Command, args []string) {
if err := userLADelete(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type userLADeleteOptions struct {
userName string
laID string
}
var userLADeleteOpts userLADeleteOptions
func init() {
flags := cmdUserLADelete.Flags()
flags.StringVarP(&userLADeleteOpts.userName, "username", "n", "", "user name")
flags.StringVar(&userLADeleteOpts.laID, "laid", "", "linked account id")
cmdUserLADelete.MarkFlagRequired("username")
cmdUserLADelete.MarkFlagRequired("laid")
cmdUserLA.AddCommand(cmdUserLADelete)
}
func userLADelete(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
userName := userLADeleteOpts.userName
laID := userLADeleteOpts.laID
log.Infof("deleting linked account %s for user %q", userName)
_, err := gwclient.DeleteUserLA(context.TODO(), userName, laID)
if err != nil {
return errors.Wrapf(err, "failed to delete linked account")
}
log.Infof("linked account %q for user %q deleted", userName, laID)
return nil
}

68
cmd/agola/cmd/userlist.go Normal file
View File

@ -0,0 +1,68 @@
// 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"
"fmt"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdUserList = &cobra.Command{
Use: "list",
Run: func(cmd *cobra.Command, args []string) {
if err := userList(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
Short: "list",
}
type userListOptions struct {
limit int
start string
}
var userListOpts userListOptions
func init() {
flags := cmdUserList.PersistentFlags()
flags.IntVar(&userListOpts.limit, "limit", 10, "max number of runs to show")
flags.StringVar(&userListOpts.start, "start", "", "starting user name (excluded) to fetch")
cmdUser.AddCommand(cmdUserList)
}
func printUsers(usersResponse *api.UsersResponse) {
for _, user := range usersResponse.Users {
fmt.Printf("%s: Name: %s\n", user.ID, user.UserName)
}
}
func userList(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
usersResponse, _, err := gwclient.GetUsers(context.TODO(), userListOpts.start, userListOpts.limit, false)
if err != nil {
return err
}
printUsers(usersResponse)
return nil
}

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 cmdUserToken = &cobra.Command{
Use: "token",
Short: "token",
}
func init() {
cmdUser.AddCommand(cmdUserToken)
}

View File

@ -0,0 +1,72 @@
// 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"
"fmt"
"github.com/pkg/errors"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/spf13/cobra"
)
var cmdUserTokenCreate = &cobra.Command{
Use: "create",
Short: "create a user linkedaccount",
Run: func(cmd *cobra.Command, args []string) {
if err := userTokenCreate(cmd, args); err != nil {
log.Fatalf("err: %v", err)
}
},
}
type userTokenCreateOptions struct {
username string
tokenName string
}
var userTokenCreateOpts userTokenCreateOptions
func init() {
flags := cmdUserTokenCreate.Flags()
flags.StringVarP(&userTokenCreateOpts.username, "username", "n", "", "user name")
flags.StringVarP(&userTokenCreateOpts.tokenName, "tokenname", "t", "", "token name")
cmdUserTokenCreate.MarkFlagRequired("username")
cmdUserTokenCreate.MarkFlagRequired("tokenname")
cmdUserToken.AddCommand(cmdUserTokenCreate)
}
func userTokenCreate(cmd *cobra.Command, args []string) error {
gwclient := api.NewClient(gatewayURL, token)
req := &api.CreateUserTokenRequest{
TokenName: userTokenCreateOpts.tokenName,
}
log.Infof("creating token for user %q", userTokenCreateOpts.username)
resp, _, err := gwclient.CreateUserToken(context.TODO(), userTokenCreateOpts.username, req)
if err != nil {
return errors.Wrapf(err, "failed to create token")
}
log.Infof("token for user %q created: %s", userTokenCreateOpts.username, resp.Token)
fmt.Println(resp.Token)
return nil
}

10
go.mod
View File

@ -1,16 +1,19 @@
module github.com/sorintlab/agola
require (
code.gitea.io/sdk v0.0.0-20190219191342-62c4fab696b4
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Masterminds/squirrel v0.0.0-20181204161840-e5bf00f96d4a
github.com/Microsoft/go-winio v0.4.11 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/bmatcuk/doublestar v1.1.1
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/elazarl/go-bindata-assetfs v1.0.0
github.com/go-bindata/go-bindata v1.0.0
github.com/go-ini/ini v1.42.0 // indirect
github.com/go-sql-driver/mysql v1.4.1 // indirect
@ -38,12 +41,11 @@ require (
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/spf13/cobra v0.0.3
github.com/xanzy/go-gitlab v0.14.1
go.etcd.io/etcd v0.0.0-20181128220305-dedae6eb7c25
go.uber.org/zap v1.9.1
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e // indirect
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
google.golang.org/appengine v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9
gopkg.in/ini.v1 v1.41.0 // indirect
gopkg.in/yaml.v2 v2.2.2
gotest.tools v2.2.0+incompatible // indirect

15
go.sum
View File

@ -1,3 +1,6 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.gitea.io/sdk v0.0.0-20190219191342-62c4fab696b4 h1:KKhwkTVL8+8/89BXwGf1kvCGXutY+QnJXNys62+Js7A=
code.gitea.io/sdk v0.0.0-20190219191342-62c4fab696b4/go.mod h1:5bZt0dRznpn2JysytQnV0yCru3FwDv9O5G91jo+lDAk=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Masterminds/squirrel v0.0.0-20181204161840-e5bf00f96d4a h1:pMmt05odIWMlrx89uWavde2DDX8SXzaYnbGW+knFeU0=
@ -34,6 +37,8 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -59,6 +64,8 @@ github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
@ -165,6 +172,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xanzy/go-gitlab v0.14.1 h1:+CipI8+oQxqWNmKCU/9GQvlQnJ5v366ChHNEI4O83is=
github.com/xanzy/go-gitlab v0.14.1/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs=
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.1-etcd.7 h1:M0l89sIuZ+RkW0rLbUsmxescVzLwLUs+Kvks+0jeHdM=
@ -182,9 +191,14 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9 h1:pfyU+l9dEu0vZzDDMsdAKa1gZbJYEn6urYXj/+Xkz7s=
golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -194,6 +208,7 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80 h1:GL7nK1hkDKrkor0eVOYcMdIsUGErFnaC2gpBOVC+vbI=

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 (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/sorintlab/agola/internal/services/types"
"github.com/pkg/errors"
)
var jsonContent = http.Header{"content-type": []string{"application/json"}}
// Client represents a Gogs API client.
type Client struct {
url string
client *http.Client
token string
}
// NewClient initializes and returns a API client.
func NewClient(url, token string) *Client {
return &Client{
url: strings.TrimSuffix(url, "/"),
client: &http.Client{},
token: token,
}
}
// SetHTTPClient replaces default http.Client with user given one.
func (c *Client) SetHTTPClient(client *http.Client) {
c.client = client
}
func (c *Client) doRequest(ctx context.Context, method, path string, query url.Values, header http.Header, ibody io.Reader) (*http.Response, error) {
u, err := url.Parse(c.url + "/api/v1alpha" + path)
if err != nil {
return nil, err
}
u.RawQuery = query.Encode()
req, err := http.NewRequest(method, u.String(), ibody)
req = req.WithContext(ctx)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "token "+c.token)
for k, v := range header {
req.Header[k] = v
}
return c.client.Do(req)
}
func (c *Client) getResponse(ctx context.Context, method, path string, query url.Values, header http.Header, ibody io.Reader) (*http.Response, error) {
resp, err := c.doRequest(ctx, method, path, query, header, ibody)
if err != nil {
return nil, err
}
if resp.StatusCode/100 != 2 {
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if len(data) <= 1 {
return resp, errors.New(resp.Status)
}
// TODO(sgotti) use a json error response
return resp, errors.New(string(data))
}
return resp, nil
}
func (c *Client) getParsedResponse(ctx context.Context, method, path string, query url.Values, header http.Header, ibody io.Reader, obj interface{}) (*http.Response, error) {
resp, err := c.getResponse(ctx, method, path, query, header, ibody)
if err != nil {
return resp, err
}
defer resp.Body.Close()
d := json.NewDecoder(resp.Body)
return resp, d.Decode(obj)
}
func (c *Client) GetProject(ctx context.Context, projectID string) (*types.Project, *http.Response, error) {
project := new(types.Project)
resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/project/%s", projectID), nil, jsonContent, nil, project)
return project, resp, err
}
func (c *Client) GetProjects(ctx context.Context, start string, limit int, asc bool) (*GetProjectsResponse, *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", "")
}
projects := new(GetProjectsResponse)
resp, err := c.getParsedResponse(ctx, "GET", "/projects", q, jsonContent, nil, &projects)
return projects, resp, err
}
func (c *Client) CreateProject(ctx context.Context, 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)
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) ReconfigProject(ctx context.Context, projectName string) (*http.Response, error) {
return c.getResponse(ctx, "POST", fmt.Sprintf("/projects/%s/reconfig", projectName), nil, jsonContent, nil)
}
func (c *Client) GetUser(ctx context.Context, userID string) (*types.User, *http.Response, error) {
user := new(types.User)
resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/user/%s", userID), nil, jsonContent, nil, user)
return user, resp, err
}
func (c *Client) GetUsers(ctx context.Context, start string, limit int, asc bool) (*UsersResponse, *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", "")
}
users := new(UsersResponse)
resp, err := c.getParsedResponse(ctx, "GET", "/users", q, jsonContent, nil, &users)
return users, resp, err
}
func (c *Client) CreateUser(ctx context.Context, req *CreateUserRequest) (*UserResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
user := new(UserResponse)
resp, err := c.getParsedResponse(ctx, "PUT", "/users", nil, jsonContent, bytes.NewReader(reqj), user)
return user, resp, err
}
func (c *Client) DeleteUser(ctx context.Context, userName string) (*http.Response, error) {
return c.getResponse(ctx, "DELETE", fmt.Sprintf("/users/%s", userName), nil, jsonContent, nil)
}
func (c *Client) CreateUserLA(ctx context.Context, userName string, req *CreateUserLARequest) (*CreateUserLAResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
la := new(CreateUserLAResponse)
resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/users/%s/linkedaccounts", userName), nil, jsonContent, bytes.NewReader(reqj), la)
return la, resp, err
}
func (c *Client) DeleteUserLA(ctx context.Context, userName, laID string) (*http.Response, error) {
return c.getResponse(ctx, "DELETE", fmt.Sprintf("/users/%s/linkedaccounts/%s", userName, laID), nil, jsonContent, nil)
}
func (c *Client) CreateUserToken(ctx context.Context, userName string, req *CreateUserTokenRequest) (*CreateUserTokenResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
tresp := new(CreateUserTokenResponse)
resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/users/%s/tokens", userName), nil, jsonContent, bytes.NewReader(reqj), tresp)
return tresp, resp, err
}
func (c *Client) GetRun(ctx context.Context, runID string) (*RunResponse, *http.Response, error) {
run := new(RunResponse)
resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/run/%s", runID), nil, jsonContent, nil, run)
return run, resp, err
}
func (c *Client) GetRuns(ctx context.Context, phaseFilter, groups, runGroups []string, start string, limit int, asc bool) (*GetRunsResponse, *http.Response, error) {
q := url.Values{}
for _, phase := range phaseFilter {
q.Add("phase", phase)
}
for _, group := range groups {
q.Add("group", group)
}
for _, runGroup := range runGroups {
q.Add("rungroup", runGroup)
}
if start != "" {
q.Add("start", start)
}
if limit > 0 {
q.Add("limit", strconv.Itoa(limit))
}
if asc {
q.Add("asc", "")
}
getRunsResponse := new(GetRunsResponse)
resp, err := c.getParsedResponse(ctx, "GET", "/runs", q, jsonContent, nil, getRunsResponse)
return getRunsResponse, resp, err
}
func (c *Client) GetRemoteSource(ctx context.Context, rsID string) (*RemoteSourceResponse, *http.Response, error) {
rs := new(RemoteSourceResponse)
resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/remotesource/%s", rsID), nil, jsonContent, nil, rs)
return rs, resp, err
}
func (c *Client) GetRemoteSources(ctx context.Context, start string, limit int, asc bool) (*RemoteSourcesResponse, *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", "")
}
rss := new(RemoteSourcesResponse)
resp, err := c.getParsedResponse(ctx, "GET", "/remotesources", q, jsonContent, nil, &rss)
return rss, resp, err
}
func (c *Client) CreateRemoteSource(ctx context.Context, req *CreateRemoteSourceRequest) (*types.RemoteSource, *http.Response, error) {
uj, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
rs := new(types.RemoteSource)
resp, err := c.getParsedResponse(ctx, "PUT", "/remotesources", nil, jsonContent, bytes.NewReader(uj), rs)
return rs, resp, err
}
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)
}

View File

@ -0,0 +1,79 @@
// 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"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/command"
"go.uber.org/zap"
)
type OAuth2CallbackHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
configstoreClient *csapi.Client
}
type RemoteSourceAuthResult struct {
RequestType string `json:"request_type,omitempty"`
Response interface{} `json:"response,omitempty"`
}
func NewOAuth2CallbackHandler(logger *zap.Logger, ch *command.CommandHandler, configstoreClient *csapi.Client) *OAuth2CallbackHandler {
return &OAuth2CallbackHandler{log: logger.Sugar(), ch: ch, configstoreClient: configstoreClient}
}
func (h *OAuth2CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
query := r.URL.Query()
code := query.Get("code")
state := query.Get("state")
cresp, err := h.ch.HandleOauth2Callback(ctx, code, state)
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var response interface{}
switch cresp.RequestType {
case "createuserla":
authresp := cresp.Response.(*command.CreateUserLAResponse)
response = &CreateUserLAResponse{
LinkedAccount: authresp.LinkedAccount,
}
case "loginuser":
authresp := cresp.Response.(*command.LoginUserResponse)
response = &LoginUserResponse{
Token: authresp.Token,
User: createUserResponse(authresp.User),
}
}
resp := RemoteSourceAuthResult{
RequestType: cresp.RequestType,
Response: response,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -0,0 +1,282 @@
// 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"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/command"
"github.com/sorintlab/agola/internal/services/types"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
type CreateProjectRequest struct {
Name string `json:"name"`
RepoURL string `json:"repo_url"`
RemoteSourceName string `json:"remote_source_name"`
SkipSSHHostKeyCheck bool `json:"skip_ssh_host_key_check"`
}
type CreateProjectHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
configstoreClient *csapi.Client
exposedURL string
}
func NewCreateProjectHandler(logger *zap.Logger, ch *command.CommandHandler, configstoreClient *csapi.Client, exposedURL string) *CreateProjectHandler {
return &CreateProjectHandler{log: logger.Sugar(), ch: ch, configstoreClient: configstoreClient, exposedURL: exposedURL}
}
func (h *CreateProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req CreateProjectRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctxUserID := ctx.Value("userid")
if ctxUserID == nil {
http.Error(w, "no userid specified", http.StatusBadRequest)
return
}
userID := ctxUserID.(string)
h.log.Infof("userID: %q", userID)
project, err := h.ch.CreateProject(ctx, &command.CreateProjectRequest{
Name: req.Name,
RepoURL: req.RepoURL,
RemoteSourceName: req.RemoteSourceName,
UserID: userID,
SkipSSHHostKeyCheck: req.SkipSSHHostKeyCheck,
})
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(project); err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type ProjectReconfigHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
configstoreClient *csapi.Client
exposedURL string
}
func NewProjectReconfigHandler(logger *zap.Logger, ch *command.CommandHandler, configstoreClient *csapi.Client, exposedURL string) *ProjectReconfigHandler {
return &ProjectReconfigHandler{log: logger.Sugar(), ch: ch, configstoreClient: configstoreClient, exposedURL: exposedURL}
}
func (h *ProjectReconfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectName := vars["projectname"]
if err := h.ch.ReconfigProject(ctx, projectName); err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type DeleteProjectHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewDeleteProjectHandler(logger *zap.Logger, configstoreClient *csapi.Client) *DeleteProjectHandler {
return &DeleteProjectHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *DeleteProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectName := vars["projectname"]
resp, err := h.configstoreClient.DeleteProject(ctx, projectName)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type ProjectHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewProjectHandler(logger *zap.Logger, configstoreClient *csapi.Client) *ProjectHandler {
return &ProjectHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *ProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectID := vars["projectid"]
project, resp, err := h.configstoreClient.GetProject(ctx, projectID)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res := createProjectResponse(project)
if err := json.NewEncoder(w).Encode(res); err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type ProjectByNameHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewProjectByNameHandler(logger *zap.Logger, configstoreClient *csapi.Client) *ProjectByNameHandler {
return &ProjectByNameHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *ProjectByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectName := vars["projectname"]
project, resp, err := h.configstoreClient.GetProjectByName(ctx, projectName)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res := createProjectResponse(project)
if err := json.NewEncoder(w).Encode(res); err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type GetProjectsResponse struct {
Projects []*ProjectResponse `json:"projects"`
}
type ProjectResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
func createProjectResponse(r *types.Project) *ProjectResponse {
run := &ProjectResponse{
ID: r.ID,
Name: r.Name,
}
return run
}
type ProjectsHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewProjectsHandler(logger *zap.Logger, configstoreClient *csapi.Client) *ProjectsHandler {
return &ProjectsHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *ProjectsHandler) 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")
csprojects, resp, err := h.configstoreClient.GetProjects(ctx, start, limit, asc)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
projects := make([]*ProjectResponse, len(csprojects))
for i, p := range csprojects {
projects[i] = createProjectResponse(p)
}
getProjectsResponse := &GetProjectsResponse{
Projects: projects,
}
if err := json.NewEncoder(w).Encode(getProjectsResponse); err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -0,0 +1,235 @@
// 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/gateway/common"
"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 CreateRemoteSourceRequest struct {
Name string `json:"name"`
APIURL string `json:"apiurl"`
Type string `json:"type"`
AuthType string `json:"auth_type"`
Oauth2ClientID string `json:"oauth_2_client_id"`
Oauth2ClientSecret string `json:"oauth_2_client_secret"`
}
type CreateRemoteSourceHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewCreateRemoteSourceHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CreateRemoteSourceHandler {
return &CreateRemoteSourceHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *CreateRemoteSourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req CreateRemoteSourceRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user, err := h.createRemoteSource(ctx, &req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *CreateRemoteSourceHandler) createRemoteSource(ctx context.Context, req *CreateRemoteSourceRequest) (*types.RemoteSource, error) {
if !util.ValidateName(req.Name) {
return nil, errors.Errorf("invalid remotesource name %q", req.Name)
}
if req.Name == "" {
return nil, errors.Errorf("remotesource name required")
}
if req.APIURL == "" {
return nil, errors.Errorf("remotesource api url required")
}
if req.Type == "" {
return nil, errors.Errorf("remotesource type required")
}
if req.AuthType == "" {
return nil, errors.Errorf("remotesource auth type required")
}
// validate if the remote source type supports the required auth type
if !common.SourceSupportsAuthType(types.RemoteSourceType(req.Type), types.RemoteSourceAuthType(req.AuthType)) {
return nil, errors.Errorf("remotesource type %q doesn't support auth type %q", req.Type, req.AuthType)
}
if req.AuthType == string(types.RemoteSourceAuthTypeOauth2) {
if req.Oauth2ClientID == "" {
return nil, errors.Errorf("remotesource oauth2 clientid required")
}
if req.Oauth2ClientSecret == "" {
return nil, errors.Errorf("remotesource oauth2 client secret required")
}
}
rs := &types.RemoteSource{
Name: req.Name,
Type: types.RemoteSourceType(req.Type),
AuthType: types.RemoteSourceAuthType(req.AuthType),
APIURL: req.APIURL,
Oauth2ClientID: req.Oauth2ClientID,
Oauth2ClientSecret: req.Oauth2ClientSecret,
}
h.log.Infof("creating remotesource")
rs, _, err := h.configstoreClient.CreateRemoteSource(ctx, rs)
if err != nil {
return nil, errors.Wrapf(err, "failed to create remotesource")
}
h.log.Infof("remotesource %s created, ID: %s", rs.Name, rs.ID)
return rs, nil
}
type RemoteSourcesResponse struct {
RemoteSources []*RemoteSourceResponse `json:"remote_sources"`
}
type RemoteSourceResponse struct {
ID string `json:"id"`
Name string `json:"name"`
AuthType string `json:"auth_type"`
}
func createRemoteSourceResponse(r *types.RemoteSource) *RemoteSourceResponse {
rs := &RemoteSourceResponse{
ID: r.ID,
Name: r.Name,
AuthType: string(r.AuthType),
}
return rs
}
type RemoteSourceHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewRemoteSourceHandler(logger *zap.Logger, configstoreClient *csapi.Client) *RemoteSourceHandler {
return &RemoteSourceHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *RemoteSourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
rsID := vars["id"]
rs, resp, err := h.configstoreClient.GetRemoteSource(ctx, rsID)
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 := createRemoteSourceResponse(rs)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type RemoteSourcesHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewRemoteSourcesHandler(logger *zap.Logger, configstoreClient *csapi.Client) *RemoteSourcesHandler {
return &RemoteSourcesHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *RemoteSourcesHandler) 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")
csRemoteSources, resp, err := h.configstoreClient.GetRemoteSources(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
}
remoteSources := make([]*RemoteSourceResponse, len(csRemoteSources))
for i, rs := range csRemoteSources {
remoteSources[i] = createRemoteSourceResponse(rs)
}
remoteSourcesResponse := &RemoteSourcesResponse{
RemoteSources: remoteSources,
}
if err := json.NewEncoder(w).Encode(remoteSourcesResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -0,0 +1,443 @@
// 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 (
"bufio"
"encoding/json"
"io"
"net/http"
"strconv"
"time"
rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api"
rstypes "github.com/sorintlab/agola/internal/services/runservice/types"
"go.uber.org/zap"
"github.com/gorilla/mux"
)
type RunsResponse struct {
ID string `json:"id"`
Counter uint64 `json:"counter"`
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
Phase rstypes.RunPhase `json:"phase"`
Result rstypes.RunResult `json:"result"`
TasksWaitingApproval []string `json:"tasks_waiting_approval"`
EnqueueTime *time.Time `json:"enqueue_time"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
type RunResponse struct {
ID string `json:"id"`
Counter uint64 `json:"counter"`
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
Phase rstypes.RunPhase `json:"phase"`
Result rstypes.RunResult `json:"result"`
Tasks map[string]*RunResponseTask `json:"tasks"`
TasksWaitingApproval []string `json:"tasks_waiting_approval"`
EnqueueTime *time.Time `json:"enqueue_time"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
type RunResponseTask struct {
ID string `json:"id"`
Name string `json:"name"`
Status rstypes.RunTaskStatus `json:"status"`
Level int `json:"level"`
Depends []*rstypes.RunConfigTaskDepend `json:"depends"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
type RunTaskResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Status rstypes.RunTaskStatus `json:"status"`
Steps []*RunTaskResponseStep `json:"steps"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
type RunTaskResponseStep struct {
Phase rstypes.ExecutorTaskPhase `json:"phase"`
Name string `json:"name"`
Command string `json:"command"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
func createRunResponse(r *rstypes.Run, rc *rstypes.RunConfig) *RunResponse {
run := &RunResponse{
ID: r.ID,
Counter: r.Counter,
Name: r.Name,
Annotations: r.Annotations,
Phase: r.Phase,
Result: r.Result,
Tasks: make(map[string]*RunResponseTask),
TasksWaitingApproval: r.TasksWaitingApproval(),
EnqueueTime: r.EnqueueTime,
StartTime: r.StartTime,
EndTime: r.EndTime,
}
for name, rt := range r.RunTasks {
rct := rc.Tasks[rt.ID]
run.Tasks[name] = createRunResponseTask(r, rt, rct)
}
return run
}
func createRunResponseTask(r *rstypes.Run, rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *RunResponseTask {
t := &RunResponseTask{
ID: rt.ID,
Name: rct.Name,
Status: rt.Status,
StartTime: rt.StartTime,
EndTime: rt.EndTime,
Level: rct.Level,
Depends: rct.Depends,
}
return t
}
func createRunTaskResponse(rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *RunTaskResponse {
t := &RunTaskResponse{
ID: rt.ID,
Name: rct.Name,
Status: rt.Status,
Steps: make([]*RunTaskResponseStep, len(rt.Steps)),
StartTime: rt.StartTime,
EndTime: rt.EndTime,
}
for i := 0; i < len(t.Steps); i++ {
s := &RunTaskResponseStep{
StartTime: rt.Steps[i].StartTime,
EndTime: rt.Steps[i].EndTime,
}
rcts := rct.Steps[i]
s.Phase = rt.Steps[i].Phase
switch rcts := rcts.(type) {
case *rstypes.RunStep:
s.Name = rcts.Name
s.Command = rcts.Command
case *rstypes.SaveToWorkspaceStep:
s.Name = "save to workspace"
case *rstypes.RestoreWorkspaceStep:
s.Name = "restore workspace"
}
t.Steps[i] = s
}
return t
}
type RunHandler struct {
log *zap.SugaredLogger
runserviceClient *rsapi.Client
}
func NewRunHandler(logger *zap.Logger, runserviceClient *rsapi.Client) *RunHandler {
return &RunHandler{log: logger.Sugar(), runserviceClient: runserviceClient}
}
func (h *RunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
runResp, resp, err := h.runserviceClient.GetRun(ctx, runID)
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 := createRunResponse(runResp.Run, runResp.RunConfig)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type RuntaskHandler struct {
log *zap.SugaredLogger
runserviceClient *rsapi.Client
}
func NewRuntaskHandler(logger *zap.Logger, runserviceClient *rsapi.Client) *RuntaskHandler {
return &RuntaskHandler{log: logger.Sugar(), runserviceClient: runserviceClient}
}
func (h *RuntaskHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
taskID := vars["taskid"]
runResp, resp, err := h.runserviceClient.GetRun(ctx, runID)
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
}
run := runResp.Run
rc := runResp.RunConfig
rt, ok := run.RunTasks[taskID]
if !ok {
http.Error(w, "", http.StatusNotFound)
return
}
rct := rc.Tasks[rt.ID]
res := createRunTaskResponse(rt, rct)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
const (
DefaultRunsLimit = 25
MaxRunsLimit = 40
)
type GetRunsResponse struct {
Runs []*RunsResponse `json:"runs"`
}
func createRunsResponse(r *rstypes.Run) *RunsResponse {
run := &RunsResponse{
ID: r.ID,
Counter: r.Counter,
Name: r.Name,
Annotations: r.Annotations,
Phase: r.Phase,
Result: r.Result,
TasksWaitingApproval: r.TasksWaitingApproval(),
EnqueueTime: r.EnqueueTime,
StartTime: r.StartTime,
EndTime: r.EndTime,
}
return run
}
type RunsHandler struct {
log *zap.SugaredLogger
runserviceClient *rsapi.Client
}
func NewRunsHandler(logger *zap.Logger, runserviceClient *rsapi.Client) *RunsHandler {
return &RunsHandler{log: logger.Sugar(), runserviceClient: runserviceClient}
}
func (h *RunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
query := r.URL.Query()
phaseFilter := query["phase"]
groups := query["group"]
changeGroups := query["changegroup"]
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")
runsResp, resp, err := h.runserviceClient.GetRuns(ctx, phaseFilter, groups, changeGroups, 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
}
runs := make([]*RunsResponse, len(runsResp.Runs))
for i, r := range runsResp.Runs {
runs[i] = createRunsResponse(r)
}
getRunsResponse := &GetRunsResponse{
Runs: runs,
}
if err := json.NewEncoder(w).Encode(getRunsResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type LogsHandler struct {
log *zap.SugaredLogger
runserviceClient *rsapi.Client
}
func NewLogsHandler(logger *zap.Logger, runserviceClient *rsapi.Client) *LogsHandler {
return &LogsHandler{log: logger.Sugar(), runserviceClient: runserviceClient}
}
func (h *LogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO(sgotti) Check authorized call from client
runID := r.URL.Query().Get("runID")
if runID == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
taskID := r.URL.Query().Get("taskID")
if taskID == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
s := r.URL.Query().Get("step")
if s == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
step, err := strconv.Atoi(s)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
follow := false
if _, ok := r.URL.Query()["follow"]; ok {
follow = true
}
stream := false
if _, ok := r.URL.Query()["stream"]; ok {
stream = true
}
if follow {
stream = true
}
resp, err := h.runserviceClient.GetLogs(ctx, runID, taskID, step, follow, stream)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, "", http.StatusInternalServerError)
return
}
if stream {
w.Header().Set("Content-Type", "text/event-stream")
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
defer resp.Body.Close()
if stream {
if err := sendLogs(w, resp.Body); err != nil {
h.log.Errorf("err: %+v", err)
return
}
} else {
if _, err := io.Copy(w, resp.Body); err != nil {
h.log.Errorf("err: %+v", err)
return
}
}
}
// sendLogs is used during streaming to flush logs lines
// TODO(sgotti) there's no need to do br.ReadBytes since the response is
// already flushed by the runservice.
func sendLogs(w io.Writer, r io.Reader) error {
br := bufio.NewReader(r)
var flusher http.Flusher
if fl, ok := w.(http.Flusher); ok {
flusher = fl
}
stop := false
for {
if stop {
return nil
}
data, err := br.ReadBytes('\n')
if err != nil {
if err != io.EOF {
return err
}
if len(data) == 0 {
return nil
}
stop = true
}
if _, err := w.Write(data); err != nil {
return err
}
if flusher != nil {
flusher.Flush()
}
}
}

View File

@ -0,0 +1,537 @@
// 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/gateway/command"
"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 CreateUserRequest struct {
UserName string `json:"username"`
}
type CreateUserHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewCreateUserHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CreateUserHandler {
return &CreateUserHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *CreateUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req CreateUserRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user, err := h.createUser(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(user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *CreateUserHandler) createUser(ctx context.Context, req *CreateUserRequest) (*UserResponse, error) {
if !util.ValidateName(req.UserName) {
return nil, errors.Errorf("invalid user name %q", req.UserName)
}
u := &types.User{
UserName: req.UserName,
}
h.log.Infof("creating user")
u, _, err := h.configstoreClient.CreateUser(ctx, u)
if err != nil {
return nil, errors.Wrapf(err, "failed to create user")
}
h.log.Infof("user %s created, ID: %s", u.UserName, u.ID)
res := createUserResponse(u)
return res, nil
}
type DeleteUserHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewDeleteUserHandler(logger *zap.Logger, configstoreClient *csapi.Client) *DeleteUserHandler {
return &DeleteUserHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *DeleteUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userName := vars["username"]
resp, err := h.configstoreClient.DeleteUser(ctx, userName)
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 CurrentUserHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewCurrentUserHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CurrentUserHandler {
return &CurrentUserHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *CurrentUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userIDVal := ctx.Value("userid")
if userIDVal == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
userID := userIDVal.(string)
user, resp, err := h.configstoreClient.GetUser(ctx, userID)
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 := createUserResponse(user)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type UserHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewUserHandler(logger *zap.Logger, configstoreClient *csapi.Client) *UserHandler {
return &UserHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userID := vars["userid"]
user, resp, err := h.configstoreClient.GetUser(ctx, userID)
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 := createUserResponse(user)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type UserByNameHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewUserByNameHandler(logger *zap.Logger, configstoreClient *csapi.Client) *UserByNameHandler {
return &UserByNameHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *UserByNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userName := vars["username"]
user, resp, err := h.configstoreClient.GetUserByName(ctx, userName)
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 := createUserResponse(user)
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type UsersResponse struct {
Users []*UserResponse `json:"users"`
}
type UserResponse struct {
ID string `json:"id"`
UserName string `json:"username"`
}
func createUserResponse(r *types.User) *UserResponse {
user := &UserResponse{
ID: r.ID,
UserName: r.UserName,
}
return user
}
type UsersHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewUsersHandler(logger *zap.Logger, configstoreClient *csapi.Client) *UsersHandler {
return &UsersHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *UsersHandler) 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")
csusers, resp, err := h.configstoreClient.GetUsers(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
}
users := make([]*UserResponse, len(csusers))
for i, p := range csusers {
users[i] = createUserResponse(p)
}
usersResponse := &UsersResponse{
Users: users,
}
if err := json.NewEncoder(w).Encode(usersResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type CreateUserLARequest struct {
RemoteSourceName string `json:"remote_source_name"`
RemoteSourceLoginName string `json:"remote_login_name"`
RemoteSourceLoginPassword string `json:"remote_login_password"`
}
type CreateUserLAResponse struct {
LinkedAccount *types.LinkedAccount `json:"linked_account"`
Oauth2Redirect string `json:"oauth2_redirect"`
}
type CreateUserLAHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
configstoreClient *csapi.Client
}
func NewCreateUserLAHandler(logger *zap.Logger, ch *command.CommandHandler, configstoreClient *csapi.Client) *CreateUserLAHandler {
return &CreateUserLAHandler{log: logger.Sugar(), ch: ch, configstoreClient: configstoreClient}
}
func (h *CreateUserLAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userName := vars["username"]
var req *CreateUserLARequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := h.createUserLA(ctx, userName, req)
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *CreateUserLAHandler) createUserLA(ctx context.Context, userName string, req *CreateUserLARequest) (*CreateUserLAResponse, error) {
remoteSourceName := req.RemoteSourceName
user, _, err := h.configstoreClient.GetUserByName(ctx, userName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get user %q", userName)
}
rs, _, err := h.configstoreClient.GetRemoteSourceByName(ctx, remoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", remoteSourceName)
}
h.log.Infof("rs: %s", util.Dump(rs))
var la *types.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
h.log.Infof("la: %s", util.Dump(la))
if la != nil {
return nil, errors.Errorf("user %q already have a linked account for remote source %q", userName, rs.Name)
}
creq := &command.CreateUserLARequest{
UserName: userName,
RemoteSourceName: rs.Name,
}
h.log.Infof("creating linked account")
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, rs, req.RemoteSourceLoginName, req.RemoteSourceLoginPassword, "createuserla", creq)
if err != nil {
return nil, err
}
if cresp.Oauth2Redirect != "" {
return &CreateUserLAResponse{
Oauth2Redirect: cresp.Oauth2Redirect,
}, nil
}
authresp := cresp.Response.(*command.CreateUserLAResponse)
resp := &CreateUserLAResponse{
LinkedAccount: authresp.LinkedAccount,
}
h.log.Infof("linked account %q for user %q created", resp.LinkedAccount.ID, userName)
return resp, nil
}
type DeleteUserLAHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewDeleteUserLAHandler(logger *zap.Logger, configstoreClient *csapi.Client) *DeleteUserLAHandler {
return &DeleteUserLAHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *DeleteUserLAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userName := vars["username"]
laID := vars["laid"]
_, err := h.configstoreClient.DeleteUserLA(ctx, userName, laID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type CreateUserTokenRequest struct {
TokenName string `json:"token_name"`
}
type CreateUserTokenResponse struct {
Token string `json:"token"`
}
type CreateUserTokenHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
}
func NewCreateUserTokenHandler(logger *zap.Logger, configstoreClient *csapi.Client) *CreateUserTokenHandler {
return &CreateUserTokenHandler{log: logger.Sugar(), configstoreClient: configstoreClient}
}
func (h *CreateUserTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userName := vars["username"]
var req CreateUserTokenRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
creq := &csapi.CreateUserTokenRequest{
TokenName: req.TokenName,
}
h.log.Infof("creating user %q token", userName)
cresp, _, err := h.configstoreClient.CreateUserToken(ctx, userName, creq)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.log.Infof("user %q token created", userName)
resp := &CreateUserTokenResponse{
Token: cresp.Token,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type LoginUserRequest struct {
RemoteSourceName string `json:"remote_source_name"`
LoginName string `json:"login_name"`
LoginPassword string `json:"password"`
}
type LoginUserResponse struct {
Oauth2Redirect string `json:"oauth2_redirect"`
Token string `json:"token"`
User *UserResponse `json:"user"`
}
type LoginUserHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
configstoreClient *csapi.Client
}
func NewLoginUserHandler(logger *zap.Logger, ch *command.CommandHandler, configstoreClient *csapi.Client) *LoginUserHandler {
return &LoginUserHandler{log: logger.Sugar(), ch: ch, configstoreClient: configstoreClient}
}
func (h *LoginUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req *LoginUserRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := h.loginUser(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(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *LoginUserHandler) loginUser(ctx context.Context, req *LoginUserRequest) (*LoginUserResponse, error) {
remoteSourceName := req.RemoteSourceName
rs, _, err := h.configstoreClient.GetRemoteSourceByName(ctx, remoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", remoteSourceName)
}
h.log.Infof("rs: %s", util.Dump(rs))
creq := &command.LoginUserRequest{
RemoteSourceName: rs.Name,
}
h.log.Infof("logging in user")
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, rs, req.LoginName, req.LoginPassword, "loginuser", creq)
if err != nil {
return nil, err
}
if cresp.Oauth2Redirect != "" {
return &LoginUserResponse{
Oauth2Redirect: cresp.Oauth2Redirect,
}, nil
}
authresp := cresp.Response.(*command.LoginUserResponse)
resp := &LoginUserResponse{
Token: authresp.Token,
User: createUserResponse(authresp.User),
}
return resp, nil
}
type RemoteSourceAuthResponse struct {
Oauth2Redirect string `json:"oauth_2_redirect"`
Response interface{} `json:"response"`
}

View File

@ -0,0 +1,40 @@
// 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 command
import (
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common"
"go.uber.org/zap"
)
type CommandHandler struct {
log *zap.SugaredLogger
sd *common.TokenSigningData
configstoreClient *csapi.Client
apiExposedURL string
webExposedURL string
}
func NewCommandHandler(logger *zap.Logger, sd *common.TokenSigningData, configstoreClient *csapi.Client, apiExposedURL, webExposedURL string) *CommandHandler {
return &CommandHandler{
log: logger.Sugar(),
sd: sd,
configstoreClient: configstoreClient,
apiExposedURL: apiExposedURL,
webExposedURL: webExposedURL,
}
}

View File

@ -0,0 +1,174 @@
// 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 command
import (
"context"
"fmt"
"net/url"
"path"
"strings"
"github.com/sorintlab/agola/internal/services/gateway/common"
"github.com/sorintlab/agola/internal/services/types"
"github.com/sorintlab/agola/internal/util"
"github.com/pkg/errors"
)
type CreateProjectRequest struct {
Name string
RemoteSourceName string
RepoURL string
UserID string
SkipSSHHostKeyCheck bool
}
func (c *CommandHandler) CreateProject(ctx context.Context, req *CreateProjectRequest) (*types.Project, error) {
if !util.ValidateName(req.Name) {
return nil, errors.Errorf("invalid project name %q", req.Name)
}
u, err := url.Parse(req.RepoURL)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse repo url")
}
repoOwner := strings.TrimPrefix(path.Dir(u.Path), "/")
repoName := path.Base(u.Path)
u.RawQuery = ""
u.Path = ""
host := u.Hostname()
c.log.Infof("repoOwner: %s, repoName: %s", repoOwner, repoName)
cloneURL := fmt.Sprintf("git@%s:%s/%s.git", host, repoOwner, repoName)
c.log.Infof("cloneURL: %s", cloneURL)
c.log.Infof("generating ssh key pairs")
privateKey, _, err := util.GenSSHKeyPair(4096)
if err != nil {
return nil, errors.Wrapf(err, "failed to generate ssh key pair")
}
user, _, err := c.configstoreClient.GetUser(ctx, req.UserID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get user %q", req.UserID)
}
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", req.RemoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
var la *types.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
c.log.Infof("la: %s", util.Dump(la))
if la == nil {
return nil, errors.Errorf("user doesn't have a linked account for remote source %q", rs.Name)
}
p := &types.Project{
Name: req.Name,
LinkedAccountID: la.ID,
Path: fmt.Sprintf("%s/%s", repoOwner, repoName),
CloneURL: cloneURL,
SkipSSHHostKeyCheck: req.SkipSSHHostKeyCheck,
SSHPrivateKey: string(privateKey),
}
c.log.Infof("creating project")
p, _, err = c.configstoreClient.CreateProject(ctx, p)
if err != nil {
return nil, errors.Wrapf(err, "failed to create project")
}
c.log.Infof("project %s created, ID: %s", p.Name, p.ID)
return p, c.SetupProject(ctx, rs, la, &SetupProjectRequest{
Project: p,
RepoOwner: repoOwner,
RepoName: repoName,
})
}
type SetupProjectRequest struct {
Project *types.Project
RepoOwner string
RepoName string
}
func (c *CommandHandler) SetupProject(ctx context.Context, rs *types.RemoteSource, la *types.LinkedAccount, conf *SetupProjectRequest) error {
c.log.Infof("setupproject")
gitsource, err := common.GetGitSource(rs, la)
pubKey, err := util.ExtractPublicKey([]byte(conf.Project.SSHPrivateKey))
if err != nil {
return errors.Wrapf(err, "failed to create gitea client")
}
webhookURL := fmt.Sprintf("%s/webhooks?projectid=%s", c.apiExposedURL, conf.Project.ID)
c.log.Infof("creating/updating deploy key: %s", string(pubKey))
if err := gitsource.UpdateDeployKey(conf.RepoOwner, conf.RepoName, "agola deploy key", string(pubKey), true); err != nil {
return errors.Wrapf(err, "failed to create deploy key")
}
c.log.Infof("deleting existing webhooks")
if err := gitsource.DeleteRepoWebhook(conf.RepoOwner, conf.RepoName, webhookURL); err != nil {
return errors.Wrapf(err, "failed to delete repository webhook")
}
c.log.Infof("creating webhook to url: %s", webhookURL)
if err := gitsource.CreateRepoWebhook(conf.RepoOwner, conf.RepoName, webhookURL, ""); err != nil {
return errors.Wrapf(err, "failed to create repository webhook")
}
return nil
}
func (c *CommandHandler) ReconfigProject(ctx context.Context, projectName string) error {
p, _, err := c.configstoreClient.GetProjectByName(ctx, projectName)
if err != nil {
return err
}
user, _, err := c.configstoreClient.GetUserByLinkedAccount(ctx, p.LinkedAccountID)
if err != nil {
return errors.Wrapf(err, "failed to get user with linked account id %q", p.LinkedAccountID)
}
la := user.LinkedAccounts[p.LinkedAccountID]
c.log.Infof("la: %s", util.Dump(la))
if la == nil {
return errors.Errorf("linked account %q in user %q doesn't exist", p.LinkedAccountID, user.UserName)
}
rs, _, err := c.configstoreClient.GetRemoteSource(ctx, la.RemoteSourceID)
if err != nil {
return errors.Wrapf(err, "failed to get remote source %q", la.RemoteSourceID)
}
repoOwner := strings.TrimPrefix(path.Dir(p.Path), "/")
repoName := path.Base(p.Path)
return c.SetupProject(ctx, rs, la, &SetupProjectRequest{
Project: p,
RepoOwner: repoOwner,
RepoName: repoName,
})
}

View File

@ -0,0 +1,347 @@
// 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 command
import (
"context"
"encoding/json"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common"
"github.com/sorintlab/agola/internal/services/types"
"github.com/sorintlab/agola/internal/util"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
)
type CreateUserLARequest struct {
UserName string
RemoteSourceName string
RemoteSourceUserAccessToken string
RemoteSourceOauth2AccessToken string
RemoteSourceOauth2RefreshToken string
}
func (c *CommandHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequest) (*types.LinkedAccount, error) {
userName := req.UserName
user, _, err := c.configstoreClient.GetUserByName(ctx, userName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get user %q", userName)
}
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", req.RemoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
var la *types.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
c.log.Infof("la: %s", util.Dump(la))
if la != nil {
return nil, errors.Errorf("user %q already have a linked account for remote source %q", userName, rs.Name)
}
accessToken, err := common.GetAccessToken(rs.AuthType, req.RemoteSourceUserAccessToken, req.RemoteSourceOauth2AccessToken)
if err != nil {
return nil, err
}
userSource, err := common.GetUserSource(rs, accessToken)
if err != nil {
return nil, err
}
remoteUserInfo, err := userSource.GetUserInfo()
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve remote user info for remote source %q", rs.ID)
}
if remoteUserInfo.ID == "" {
return nil, errors.Errorf("empty remote user id for remote source %q", rs.ID)
}
creq := &csapi.CreateUserLARequest{
RemoteSourceName: req.RemoteSourceName,
RemoteUserID: remoteUserInfo.ID,
RemoteUserName: remoteUserInfo.LoginName,
Oauth2AccessToken: req.RemoteSourceOauth2AccessToken,
Oauth2RefreshToken: req.RemoteSourceOauth2RefreshToken,
UserAccessToken: req.RemoteSourceUserAccessToken,
}
c.log.Infof("creating linked account")
la, _, err = c.configstoreClient.CreateUserLA(ctx, userName, creq)
if err != nil {
return nil, errors.Wrapf(err, "failed to create linked account")
}
c.log.Infof("linked account %q for user %q created", la.ID, userName)
return la, nil
}
type LoginUserRequest struct {
RemoteSourceName string
RemoteSourceUserAccessToken string
RemoteSourceOauth2AccessToken string
RemoteSourceOauth2RefreshToken string
}
type LoginUserResponse struct {
Token string
User *types.User
}
func (c *CommandHandler) LoginUser(ctx context.Context, req *LoginUserRequest) (*LoginUserResponse, error) {
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", req.RemoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
accessToken, err := common.GetAccessToken(rs.AuthType, req.RemoteSourceUserAccessToken, req.RemoteSourceOauth2AccessToken)
if err != nil {
return nil, err
}
userSource, err := common.GetUserSource(rs, accessToken)
if err != nil {
return nil, err
}
remoteUserInfo, err := userSource.GetUserInfo()
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve remote user info for remote source %q", rs.ID)
}
if remoteUserInfo.ID == "" {
return nil, errors.Errorf("empty remote user id for remote source %q", rs.ID)
}
user, _, err := c.configstoreClient.GetUserByLinkedAccountRemoteUserAndSource(ctx, remoteUserInfo.ID, rs.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to get user for remote user id %q and remote source %q", remoteUserInfo.ID, rs.ID)
}
var la *types.LinkedAccount
for _, v := range user.LinkedAccounts {
if v.RemoteSourceID == rs.ID {
la = v
break
}
}
c.log.Infof("la: %s", util.Dump(la))
if la == nil {
return nil, errors.Errorf("linked account for user %q for remote source %q doesn't exist", user.UserName, rs.Name)
}
// Update oauth tokens if they have changed since the getuserinfo request may have updated them
if la.Oauth2AccessToken != req.RemoteSourceOauth2AccessToken ||
la.Oauth2RefreshToken != req.RemoteSourceOauth2RefreshToken ||
la.UserAccessToken != req.RemoteSourceUserAccessToken {
la.Oauth2AccessToken = req.RemoteSourceOauth2AccessToken
la.Oauth2RefreshToken = req.RemoteSourceOauth2RefreshToken
la.UserAccessToken = req.RemoteSourceUserAccessToken
creq := &csapi.UpdateUserLARequest{
RemoteUserID: la.RemoteUserID,
RemoteUserName: la.RemoteUserName,
Oauth2AccessToken: la.Oauth2AccessToken,
Oauth2RefreshToken: la.Oauth2RefreshToken,
UserAccessToken: la.UserAccessToken,
}
c.log.Infof("updating user %q linked account", user.UserName)
la, _, err = c.configstoreClient.UpdateUserLA(ctx, user.UserName, la.ID, creq)
if err != nil {
return nil, errors.Wrapf(err, "failed to update user")
}
c.log.Infof("linked account %q for user %q updated", la.ID, user.UserName)
}
// generate jwt token
token, err := common.GenerateLoginJWTToken(c.sd, user.ID)
if err != nil {
return nil, err
}
return &LoginUserResponse{
Token: token,
User: user,
}, nil
}
type RemoteSourceAuthResponse struct {
Oauth2Redirect string
Response interface{}
}
func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, rs *types.RemoteSource, loginName, loginPassword, requestType string, req interface{}) (*RemoteSourceAuthResponse, error) {
switch rs.AuthType {
case types.RemoteSourceAuthTypeOauth2:
oauth2Source, err := common.GetOauth2Source(rs, "")
if err != nil {
return nil, errors.Wrapf(err, "failed to create git source")
}
token, err := common.GenerateJWTToken(c.sd, rs.Name, requestType, req)
if err != nil {
return nil, err
}
redirect, err := oauth2Source.GetOauth2AuthorizationURL(c.webExposedURL+"/oauth2/callback", token)
if err != nil {
return nil, err
}
c.log.Infof("oauth2 redirect: %s", redirect)
return &RemoteSourceAuthResponse{
Oauth2Redirect: redirect,
}, nil
case types.RemoteSourceAuthTypePassword:
passwordSource, err := common.GetPasswordSource(rs, "")
if err != nil {
return nil, errors.Wrapf(err, "failed to create git source")
}
accessToken, err := passwordSource.LoginPassword(loginName, loginPassword)
if err != nil {
return nil, errors.Wrapf(err, "failed to login to remote source %q with login name %q", rs.Name, loginName)
}
c.log.Infof("access token: %s", accessToken)
requestj, err := json.Marshal(req)
if err != nil {
return nil, err
}
cres, err := c.HandleRemoteSourceAuthRequest(ctx, requestType, string(requestj), accessToken, "", "")
if err != nil {
return nil, err
}
return &RemoteSourceAuthResponse{
Response: cres.Response,
}, nil
default:
return nil, errors.Errorf("unknown remote source authentication type: %q", rs.AuthType)
}
}
type RemoteSourceAuthResult struct {
RequestType string
Response interface{}
}
type CreateUserLAResponse struct {
LinkedAccount *types.LinkedAccount
}
func (c *CommandHandler) HandleRemoteSourceAuthRequest(ctx context.Context, requestType, requestString string, userAccessToken, Oauth2AccessToken, Oauth2RefreshToken string) (*RemoteSourceAuthResult, error) {
switch requestType {
case "createuserla":
var req *CreateUserLARequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
}
creq := &CreateUserLARequest{
UserName: req.UserName,
RemoteSourceName: req.RemoteSourceName,
RemoteSourceUserAccessToken: userAccessToken,
RemoteSourceOauth2AccessToken: Oauth2AccessToken,
RemoteSourceOauth2RefreshToken: Oauth2RefreshToken,
}
la, err := c.CreateUserLA(ctx, creq)
if err != nil {
return nil, err
}
return &RemoteSourceAuthResult{
RequestType: requestType,
Response: &CreateUserLAResponse{
LinkedAccount: la,
},
}, nil
case "loginuser":
var req *LoginUserRequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
}
creq := &LoginUserRequest{
RemoteSourceName: req.RemoteSourceName,
RemoteSourceUserAccessToken: userAccessToken,
RemoteSourceOauth2AccessToken: Oauth2AccessToken,
RemoteSourceOauth2RefreshToken: Oauth2RefreshToken,
}
cresp, err := c.LoginUser(ctx, creq)
if err != nil {
return nil, err
}
return &RemoteSourceAuthResult{
RequestType: requestType,
Response: &LoginUserResponse{
Token: cresp.Token,
User: cresp.User,
},
}, nil
default:
return nil, errors.Errorf("unknown request")
}
}
func (c *CommandHandler) HandleOauth2Callback(ctx context.Context, code, state string) (*RemoteSourceAuthResult, error) {
token, err := jwt.Parse(state, func(token *jwt.Token) (interface{}, error) {
if token.Method != c.sd.Method {
return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"])
}
var key interface{}
switch c.sd.Method {
case jwt.SigningMethodRS256:
key = c.sd.PrivateKey
case jwt.SigningMethodHS256:
key = c.sd.Key
default:
return nil, errors.Errorf("unsupported signing method %q", c.sd.Method.Alg())
}
return key, nil
})
if err != nil {
return nil, errors.Wrap(err, "failed to parse jwt")
}
if !token.Valid {
return nil, errors.Errorf("invalid token")
}
claims := token.Claims.(jwt.MapClaims)
remoteSourceName := claims["remote_source_name"].(string)
requestType := claims["request_type"].(string)
requestString := claims["request"].(string)
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, remoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", remoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
oauth2Source, err := common.GetOauth2Source(rs, "")
if err != nil {
return nil, errors.Wrapf(err, "failed to create gitlab source")
}
oauth2Token, err := oauth2Source.RequestOauth2Token(c.webExposedURL+"/oauth2/callback", code)
if err != nil {
return nil, err
}
return c.HandleRemoteSourceAuthRequest(ctx, requestType, requestString, "", oauth2Token.AccessToken, oauth2Token.RefreshToken)
}

View File

@ -0,0 +1,143 @@
// 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 common
import (
"fmt"
"github.com/pkg/errors"
gitsource "github.com/sorintlab/agola/internal/gitsources"
"github.com/sorintlab/agola/internal/gitsources/gitea"
"github.com/sorintlab/agola/internal/gitsources/gitlab"
"github.com/sorintlab/agola/internal/services/types"
)
func SourceSupportedAuthTypes(rsType types.RemoteSourceType) []types.RemoteSourceAuthType {
switch rsType {
case types.RemoteSourceTypeGitea:
return []types.RemoteSourceAuthType{types.RemoteSourceAuthTypePassword}
case types.RemoteSourceTypeGithub:
fallthrough
case types.RemoteSourceTypeGitlab:
return []types.RemoteSourceAuthType{types.RemoteSourceAuthTypeOauth2}
default:
panic(fmt.Errorf("unsupported remote source type: %q", rsType))
}
}
func SourceSupportsAuthType(rsType types.RemoteSourceType, authType types.RemoteSourceAuthType) bool {
supportedAuthTypes := SourceSupportedAuthTypes(rsType)
for _, st := range supportedAuthTypes {
if st == authType {
return true
}
}
return false
}
func newGitea(rs *types.RemoteSource, accessToken string) (*gitea.Client, error) {
return gitea.New(gitea.Opts{
URL: rs.APIURL,
SkipVerify: rs.SkipVerify,
Token: accessToken,
})
}
func newGitlab(rs *types.RemoteSource, accessToken string) (*gitlab.Client, error) {
return gitlab.New(gitlab.Opts{
URL: rs.APIURL,
SkipVerify: rs.SkipVerify,
Token: accessToken,
Oauth2ClientID: rs.Oauth2ClientID,
Oauth2Secret: rs.Oauth2ClientSecret,
})
}
func GetAccessToken(authType types.RemoteSourceAuthType, userAccessToken, oauth2AccessToken string) (string, error) {
switch authType {
case types.RemoteSourceAuthTypePassword:
return userAccessToken, nil
case types.RemoteSourceAuthTypeOauth2:
return oauth2AccessToken, nil
default:
return "", errors.Errorf("invalid remote source auth type %q", authType)
}
}
func GetGitSource(rs *types.RemoteSource, la *types.LinkedAccount) (gitsource.GitSource, error) {
var accessToken string
if la != nil {
var err error
accessToken, err = GetAccessToken(rs.AuthType, la.UserAccessToken, la.Oauth2AccessToken)
if err != nil {
return nil, err
}
}
var gitSource gitsource.GitSource
var err error
switch rs.Type {
case types.RemoteSourceTypeGitea:
gitSource, err = newGitea(rs, accessToken)
case types.RemoteSourceTypeGitlab:
gitSource, err = newGitlab(rs, accessToken)
default:
return nil, errors.Errorf("remote source %s isn't a valid git source", rs.Name)
}
return gitSource, err
}
func GetUserSource(rs *types.RemoteSource, accessToken string) (gitsource.UserSource, error) {
var userSource gitsource.UserSource
var err error
switch rs.AuthType {
case types.RemoteSourceAuthTypeOauth2:
userSource, err = GetOauth2Source(rs, accessToken)
case types.RemoteSourceAuthTypePassword:
userSource, err = GetPasswordSource(rs, accessToken)
default:
return nil, errors.Errorf("unknown remote source auth type")
}
return userSource, err
}
func GetOauth2Source(rs *types.RemoteSource, accessToken string) (gitsource.Oauth2Source, error) {
var oauth2Source gitsource.Oauth2Source
var err error
switch rs.Type {
case types.RemoteSourceTypeGitlab:
oauth2Source, err = newGitlab(rs, accessToken)
default:
return nil, errors.Errorf("remote source %s isn't a valid oauth2 source", rs.Name)
}
return oauth2Source, err
}
func GetPasswordSource(rs *types.RemoteSource, accessToken string) (gitsource.PasswordSource, error) {
var passwordSource gitsource.PasswordSource
var err error
switch rs.Type {
case types.RemoteSourceTypeGitea:
passwordSource, err = newGitea(rs, accessToken)
default:
return nil, errors.Errorf("remote source %s isn't a valid oauth2 source", rs.Name)
}
return passwordSource, err
}

View File

@ -0,0 +1,77 @@
// 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 common
import (
"crypto/rsa"
"encoding/json"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
)
type TokenSigningData struct {
Duration time.Duration
Method jwt.SigningMethod
PrivateKey *rsa.PrivateKey
PublicKey *rsa.PublicKey
Key []byte
}
func GenerateJWTToken(sd *TokenSigningData, remoteSourceName, requestType string, request interface{}) (string, error) {
requestj, err := json.Marshal(request)
if err != nil {
return "", err
}
token := jwt.NewWithClaims(sd.Method, jwt.MapClaims{
"exp": time.Now().Add(sd.Duration).Unix(),
"remote_source_name": remoteSourceName,
"request_type": requestType,
"request": string(requestj),
})
var key interface{}
switch sd.Method {
case jwt.SigningMethodRS256:
key = sd.PrivateKey
case jwt.SigningMethodHS256:
key = sd.Key
default:
errors.Errorf("unsupported signing method %q", sd.Method.Alg())
}
// Sign and get the complete encoded token as a string
return token.SignedString(key)
}
func GenerateLoginJWTToken(sd *TokenSigningData, userID string) (string, error) {
token := jwt.NewWithClaims(sd.Method, jwt.MapClaims{
"sub": userID,
"exp": time.Now().Add(sd.Duration).Unix(),
})
var key interface{}
switch sd.Method {
case jwt.SigningMethodRS256:
key = sd.PrivateKey
case jwt.SigningMethodHS256:
key = sd.Key
default:
errors.Errorf("unsupported signing method %q", sd.Method.Alg())
}
// Sign and get the complete encoded token as a string
return token.SignedString(key)
}

View File

@ -0,0 +1,253 @@
// 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 gateway
import (
"context"
"crypto/tls"
"io/ioutil"
"net/http"
scommon "github.com/sorintlab/agola/internal/common"
slog "github.com/sorintlab/agola/internal/log"
"github.com/sorintlab/agola/internal/objectstorage"
"github.com/sorintlab/agola/internal/services/config"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/api"
"github.com/sorintlab/agola/internal/services/gateway/command"
"github.com/sorintlab/agola/internal/services/gateway/common"
"github.com/sorintlab/agola/internal/services/gateway/handlers"
rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api"
"github.com/sorintlab/agola/internal/util"
jwt "github.com/dgrijalva/jwt-go"
ghandlers "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
var logger = slog.New(level)
var log = logger.Sugar()
type Gateway struct {
c *config.Gateway
lts *objectstorage.ObjStorage
runserviceClient *rsapi.Client
configstoreClient *csapi.Client
ch *command.CommandHandler
sd *common.TokenSigningData
}
func NewGateway(c *config.Gateway) (*Gateway, error) {
if c.Debug {
level.SetLevel(zapcore.DebugLevel)
}
if c.Web.ListenAddress == "" {
return nil, errors.Errorf("listen address undefined")
}
if c.Web.TLS {
if c.Web.TLSKeyFile == "" {
return nil, errors.Errorf("no tls key file specified")
}
if c.Web.TLSCertFile == "" {
return nil, errors.Errorf("no tls cert file specified")
}
}
sd := &common.TokenSigningData{Duration: c.TokenSigning.Duration}
switch c.TokenSigning.Method {
case "hmac":
sd.Method = jwt.SigningMethodHS256
if c.TokenSigning.Key == "" {
return nil, errors.Errorf("empty token signing key for hmac method")
}
sd.Key = []byte(c.TokenSigning.Key)
case "rsa":
if c.TokenSigning.PrivateKeyPath == "" {
return nil, errors.Errorf("token signing private key file for rsa method not defined")
}
if c.TokenSigning.PublicKeyPath == "" {
return nil, errors.Errorf("token signing public key file for rsa method not defined")
}
sd.Method = jwt.SigningMethodRS256
privateKeyData, err := ioutil.ReadFile(c.TokenSigning.PrivateKeyPath)
if err != nil {
return nil, errors.Wrapf(err, "error reading token signing private key")
}
sd.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyData)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token signing private key")
}
publicKeyData, err := ioutil.ReadFile(c.TokenSigning.PublicKeyPath)
if err != nil {
return nil, errors.Wrapf(err, "error reading token signing public key")
}
sd.PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(publicKeyData)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token signing public key")
}
case "":
return nil, errors.Errorf("missing token signing method")
default:
return nil, errors.Errorf("unknown token signing method: %q", c.TokenSigning.Method)
}
lts, err := scommon.NewLTS(&c.LTS)
if err != nil {
return nil, err
}
configstoreClient := csapi.NewClient(c.ConfigStoreURL)
ch := command.NewCommandHandler(logger, sd, configstoreClient, c.APIExposedURL, c.WebExposedURL)
return &Gateway{
c: c,
lts: lts,
runserviceClient: rsapi.NewClient(c.RunServiceURL),
configstoreClient: configstoreClient,
ch: ch,
sd: sd,
}, nil
}
func (g *Gateway) Run(ctx context.Context) error {
// noop coors handler
corsHandler := func(h http.Handler) http.Handler {
return h
}
corsAllowedMethodsOptions := ghandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE"})
corsAllowedHeadersOptions := ghandlers.AllowedHeaders([]string{"Accept", "Accept-Encoding", "Authorization", "Content-Length", "Content-Type", "X-CSRF-Token", "Authorization"})
corsAllowedOriginsOptions := ghandlers.AllowedOrigins([]string{"*"})
corsHandler = ghandlers.CORS(corsAllowedMethodsOptions, corsAllowedHeadersOptions, corsAllowedOriginsOptions)
webhooksHandler := &webhooksHandler{log: log, configstoreClient: g.configstoreClient, runserviceClient: g.runserviceClient, apiExposedURL: g.c.APIExposedURL}
projectHandler := api.NewProjectHandler(logger, g.configstoreClient)
projectByNameHandler := api.NewProjectByNameHandler(logger, g.configstoreClient)
projectsHandler := api.NewProjectsHandler(logger, g.configstoreClient)
createProjectHandler := api.NewCreateProjectHandler(logger, g.ch, g.configstoreClient, g.c.APIExposedURL)
deleteProjectHandler := api.NewDeleteProjectHandler(logger, g.configstoreClient)
projectReconfigHandler := api.NewProjectReconfigHandler(logger, g.ch, g.configstoreClient, g.c.APIExposedURL)
currentUserHandler := api.NewCurrentUserHandler(logger, g.configstoreClient)
userHandler := api.NewUserHandler(logger, g.configstoreClient)
userByNameHandler := api.NewUserByNameHandler(logger, g.configstoreClient)
usersHandler := api.NewUsersHandler(logger, g.configstoreClient)
createUserHandler := api.NewCreateUserHandler(logger, g.configstoreClient)
deleteUserHandler := api.NewDeleteUserHandler(logger, g.configstoreClient)
createUserLAHandler := api.NewCreateUserLAHandler(logger, g.ch, g.configstoreClient)
deleteUserLAHandler := api.NewDeleteUserLAHandler(logger, g.configstoreClient)
createUserTokenHandler := api.NewCreateUserTokenHandler(logger, g.configstoreClient)
remoteSourceHandler := api.NewRemoteSourceHandler(logger, g.configstoreClient)
createRemoteSourceHandler := api.NewCreateRemoteSourceHandler(logger, g.configstoreClient)
remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, g.configstoreClient)
runHandler := api.NewRunHandler(logger, g.runserviceClient)
runsHandler := api.NewRunsHandler(logger, g.runserviceClient)
runtaskHandler := api.NewRuntaskHandler(logger, g.runserviceClient)
logsHandler := api.NewLogsHandler(logger, g.runserviceClient)
loginUserHandler := api.NewLoginUserHandler(logger, g.ch, g.configstoreClient)
oauth2callbackHandler := api.NewOAuth2CallbackHandler(logger, g.ch, g.configstoreClient)
router := mux.NewRouter()
apirouter := mux.NewRouter().PathPrefix("/api/v1alpha").Subrouter()
authForcedHandler := handlers.NewAuthHandler(logger, g.configstoreClient, g.c.AdminToken, g.sd, true)
authOptionalHandler := handlers.NewAuthHandler(logger, g.configstoreClient, g.c.AdminToken, g.sd, false)
router.PathPrefix("/api/v1alpha").Handler(apirouter)
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", authForcedHandler(currentUserHandler)).Methods("GET")
apirouter.Handle("/user/{userid}", authForcedHandler(userHandler)).Methods("GET")
apirouter.Handle("/users", authForcedHandler(usersHandler)).Methods("GET")
apirouter.Handle("/users", authForcedHandler(createUserHandler)).Methods("PUT")
apirouter.Handle("/users/{username}", authForcedHandler(userByNameHandler)).Methods("GET")
apirouter.Handle("/users/{username}", authForcedHandler(deleteUserHandler)).Methods("DELETE")
apirouter.Handle("/users/{username}/linkedaccounts", authForcedHandler(createUserLAHandler)).Methods("PUT")
apirouter.Handle("/users/{username}/linkedaccounts/{laid}", authForcedHandler(deleteUserLAHandler)).Methods("DELETE")
apirouter.Handle("/users/{username}/tokens", authForcedHandler(createUserTokenHandler)).Methods("PUT")
apirouter.Handle("/remotesource/{id}", authForcedHandler(remoteSourceHandler)).Methods("GET")
apirouter.Handle("/remotesources", authForcedHandler(createRemoteSourceHandler)).Methods("PUT")
apirouter.Handle("/remotesources", authOptionalHandler(remoteSourcesHandler)).Methods("GET")
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")
router.Handle("/login", loginUserHandler).Methods("POST")
router.Handle("/oauth2/callback", oauth2callbackHandler).Methods("GET")
router.Handle("/webhooks", webhooksHandler).Methods("POST")
router.PathPrefix("/").HandlerFunc(handlers.NewWebBundleHandlerFunc(g.c.APIExposedURL))
mainrouter := mux.NewRouter()
mainrouter.PathPrefix("/").Handler(corsHandler(router))
var tlsConfig *tls.Config
if g.c.Web.TLS {
var err error
tlsConfig, err = util.NewTLSConfig(g.c.Web.TLSCertFile, g.c.Web.TLSKeyFile, "", false)
if err != nil {
log.Errorf("err: %+v")
return err
}
}
httpServer := http.Server{
Addr: g.c.Web.ListenAddress,
Handler: mainrouter,
TLSConfig: tlsConfig,
}
lerrCh := make(chan error)
go func() {
lerrCh <- httpServer.ListenAndServe()
}()
select {
case <-ctx.Done():
log.Infof("configstore exiting")
httpServer.Close()
return nil
case err := <-lerrCh:
log.Errorf("http server listen error: %v", err)
return err
}
}

View File

@ -0,0 +1,170 @@
// 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 handlers
import (
"context"
"net/http"
"strings"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common"
jwt "github.com/dgrijalva/jwt-go"
jwtrequest "github.com/dgrijalva/jwt-go/request"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type AuthHandler struct {
log *zap.SugaredLogger
next http.Handler
configstoreClient *csapi.Client
adminToken string
sd *common.TokenSigningData
required bool
}
func NewAuthHandler(logger *zap.Logger, configstoreClient *csapi.Client, adminToken string, sd *common.TokenSigningData, required bool) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &AuthHandler{
log: logger.Sugar(),
next: h,
configstoreClient: configstoreClient,
adminToken: adminToken,
sd: sd,
required: required,
}
}
}
func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tokenString, _ := TokenExtractor.ExtractToken(r)
if h.adminToken != "" && tokenString != "" {
if tokenString == h.adminToken {
ctx = context.WithValue(ctx, "admin", true)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
} else {
user, resp, err := h.configstoreClient.GetUserByToken(ctx, tokenString)
if err != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, "", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
// pass userid to handlers via context
ctx = context.WithValue(ctx, "userid", user.ID)
ctx = context.WithValue(ctx, "username", user.UserName)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
tokenString, _ = BearerTokenExtractor.ExtractToken(r)
if tokenString != "" {
token, err := jwtrequest.ParseFromRequest(r, jwtrequest.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
sd := h.sd
if token.Method != sd.Method {
return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"])
}
var key interface{}
switch sd.Method {
case jwt.SigningMethodRS256:
key = sd.PrivateKey
case jwt.SigningMethodHS256:
key = sd.Key
default:
return nil, errors.Errorf("unsupported signing method %q", sd.Method.Alg())
}
return key, nil
})
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, "", http.StatusUnauthorized)
return
}
if !token.Valid {
http.Error(w, "", http.StatusUnauthorized)
return
}
// Set username in the request context
claims := token.Claims.(jwt.MapClaims)
userID := claims["sub"].(string)
user, resp, err := h.configstoreClient.GetUser(ctx, userID)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
http.Error(w, "", http.StatusUnauthorized)
return
}
http.Error(w, "", http.StatusInternalServerError)
return
}
// pass userid to handlers via context
ctx = context.WithValue(ctx, "userid", user.ID)
ctx = context.WithValue(ctx, "username", user.UserName)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
}
if h.required {
http.Error(w, "", http.StatusUnauthorized)
return
}
h.next.ServeHTTP(w, r.WithContext(ctx))
}
func stripPrefixFromTokenString(prefix string) func(tok string) (string, error) {
return func(tok string) (string, error) {
pl := len(prefix)
if len(tok) > pl && strings.ToUpper(tok[0:pl+1]) == strings.ToUpper(prefix+" ") {
return tok[pl+1:], nil
}
return "", nil
}
}
// TokenExtractor extracts a token in format "token THETOKEN" from Authorization
// header
// Uses PostExtractionFilter to strip "token " prefix from header
var TokenExtractor = &jwtrequest.PostExtractionFilter{
jwtrequest.MultiExtractor{
jwtrequest.HeaderExtractor{"Authorization"},
jwtrequest.ArgumentExtractor{"access_token"},
},
stripPrefixFromTokenString("token"),
}
// BearerTokenExtractor extracts a bearer token in format "bearer THETOKEN" from
// Authorization header
// Uses PostExtractionFilter to strip "Bearer " prefix from header
var BearerTokenExtractor = &jwtrequest.PostExtractionFilter{
jwtrequest.MultiExtractor{
jwtrequest.HeaderExtractor{"Authorization"},
jwtrequest.ArgumentExtractor{"access_token"},
},
stripPrefixFromTokenString("bearer"),
}

View File

@ -0,0 +1,96 @@
// 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 handlers
import (
"bytes"
"net/http"
"strings"
"text/template"
"github.com/sorintlab/agola/webbundle"
assetfs "github.com/elazarl/go-bindata-assetfs"
)
// TODO(sgotti) now the test web ui directly calls the run api url, but this is
// temporary and all requests should pass from the gateway
const configTplText = `
const CONFIG = {
API_URL: '{{.ApiURL}}',
API_BASE_PATH: '{{.ApiBasePath}}',
authType: '{{.AuthType}}'
}
window.CONFIG = CONFIG
`
func NewWebBundleHandlerFunc(gatewayURL string) func(w http.ResponseWriter, r *http.Request) {
var buf bytes.Buffer
configTpl, err := template.New("config").Parse(configTplText)
if err != nil {
panic(err)
}
configTplData := struct {
ApiURL string
ApiBasePath string
AuthType string
}{
gatewayURL,
"/api/v1alpha",
"local",
}
configTpl.Execute(&buf, configTplData)
config := buf.Bytes()
return func(w http.ResponseWriter, r *http.Request) {
// Setup serving of bundled webapp from the root path, registered after api
// handlers or it'll match all the requested paths
fileServerHandler := http.FileServer(&assetfs.AssetFS{
Asset: webbundle.Asset,
AssetDir: webbundle.AssetDir,
AssetInfo: webbundle.AssetInfo,
})
// check if the required file is available in the webapp asset and serve it
if _, err := webbundle.Asset(r.URL.Path[1:]); err == nil {
fileServerHandler.ServeHTTP(w, r)
return
}
// config.js is the external webapp config file not provided by the
// asset and not needed when served from the api server
if r.URL.Path == "/config.js" {
_, err := w.Write(config)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
}
return
}
// skip /api requests
if strings.HasPrefix(r.URL.Path, "/api/") {
http.Error(w, "", http.StatusNotFound)
return
}
// Fallback to index.html for every other page. Required for the SPA since
// on browser reload it'll ask the current app url but we have to
// provide the index.html
r.URL.Path = "/"
fileServerHandler.ServeHTTP(w, r)
}
}

View File

@ -0,0 +1,247 @@
// 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 gateway
import (
"context"
"fmt"
"net/http"
"net/url"
"path"
"github.com/sorintlab/agola/internal/config"
gitsource "github.com/sorintlab/agola/internal/gitsources"
"github.com/sorintlab/agola/internal/runconfig"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common"
rsapi "github.com/sorintlab/agola/internal/services/runservice/scheduler/api"
"github.com/sorintlab/agola/internal/services/types"
"github.com/sorintlab/agola/internal/util"
"github.com/pkg/errors"
"go.uber.org/zap"
)
const (
agolaDefaultConfigPath = ".agola/config.yml"
// List of runs annotations
AnnotationEventType = "event_type"
AnnotationRunType = "runtype"
AnnotationProjectID = "projectid"
AnnotationUserID = "userid"
// AnnotationVirtualBranch represent a "virtual branch": i.e a normal branch, a pr (with name pr-$prid), a tag (with name tag-tagname)
AnnotationVirtualBranch = "virtual_branch"
AnnotationCommitSHA = "commit_sha"
AnnotationRef = "ref"
AnnotationSender = "sender"
AnnotationMessage = "message"
AnnotationCommitLink = "commit_link"
AnnotationCompareLink = "compare_link"
AnnotationBranch = "branch"
AnnotationBranchLink = "branch_link"
AnnotationTag = "tag"
AnnotationTagLink = "tag_link"
AnnotationPullRequestID = "pull_request_id"
AnnotationPullRequestLink = "pull_request_link"
)
func genAnnotationVirtualBranch(webhookData *types.WebhookData) string {
switch webhookData.Event {
case types.WebhookEventPush:
return "branch-" + webhookData.Branch
case types.WebhookEventTag:
return "tag-" + webhookData.Tag
case types.WebhookEventPullRequest:
return "pr-" + webhookData.PullRequestID
}
panic(fmt.Errorf("invalid webhook event type: %q", webhookData.Event))
}
func genGroup(baseGroupID string, webhookData *types.WebhookData) string {
// we pathescape the branch name to handle branches with slashes and make the
// branch a single path entry
switch webhookData.Event {
case types.WebhookEventPush:
return path.Join(baseGroupID, "branch-"+url.PathEscape(webhookData.Branch))
case types.WebhookEventTag:
return path.Join(baseGroupID, "tag-"+url.PathEscape(webhookData.Tag))
case types.WebhookEventPullRequest:
return path.Join(baseGroupID, "pr-"+url.PathEscape(webhookData.PullRequestID))
}
panic(fmt.Errorf("invalid webhook event type: %q", webhookData.Event))
}
type webhooksHandler struct {
log *zap.SugaredLogger
configstoreClient *csapi.Client
runserviceClient *rsapi.Client
apiExposedURL string
}
func (h *webhooksHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, userErr, err := h.handleWebhook(r)
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, userErr, code)
}
}
func (h *webhooksHandler) handleWebhook(r *http.Request) (int, string, error) {
ctx := r.Context()
projectID := r.URL.Query().Get("projectid")
defer r.Body.Close()
var gitSource gitsource.GitSource
project, _, err := h.configstoreClient.GetProject(ctx, projectID)
if err != nil {
return http.StatusBadRequest, "", errors.Wrapf(err, "failed to get project %s", projectID)
}
h.log.Debugf("project: %s", util.Dump(project))
user, _, err := h.configstoreClient.GetUserByLinkedAccount(ctx, project.LinkedAccountID)
if err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get user by linked account %q", project.LinkedAccountID)
}
la := user.LinkedAccounts[project.LinkedAccountID]
h.log.Infof("la: %s", util.Dump(la))
if la == nil {
return http.StatusInternalServerError, "", errors.Errorf("linked account %q in user %q doesn't exist", project.LinkedAccountID, user.UserName)
}
rs, _, err := h.configstoreClient.GetRemoteSource(ctx, la.RemoteSourceID)
if err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to get remote source %q", la.RemoteSourceID)
}
gitSource, err = common.GetGitSource(rs, la)
if err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to create gitea client")
}
sshPrivKey := project.SSHPrivateKey
cloneURL := project.CloneURL
skipSSHHostKeyCheck := project.SkipSSHHostKeyCheck
runType := types.RunTypeProject
webhookData, err := gitSource.ParseWebhook(r)
if err != nil {
return http.StatusBadRequest, "", errors.Wrapf(err, "failed to parse webhook")
}
webhookData.ProjectID = projectID
h.log.Infof("webhookData: %s", util.Dump(webhookData))
var data []byte
err = util.ExponentialBackoff(util.FetchFileBackoff, func() (bool, error) {
var err error
data, err = gitSource.GetFile(webhookData.Repo.Owner, webhookData.Repo.Name, webhookData.CommitSHA, agolaDefaultConfigPath)
if err == nil {
return true, nil
}
h.log.Errorf("get file err: %v", err)
return false, nil
})
if err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to fetch config file")
}
h.log.Debug("data: %s", data)
gitURL, err := util.ParseGitURL(cloneURL)
if err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to parse clone url")
}
env := map[string]string{
"CI": "true",
"AGOLA_SSHPRIVKEY": sshPrivKey,
"AGOLA_REPOSITORY_URL": cloneURL,
"AGOLA_GIT_HOST": gitURL.Host,
"AGOLA_GIT_REF": webhookData.Ref,
"AGOLA_GIT_COMMITSHA": webhookData.CommitSHA,
}
if skipSSHHostKeyCheck {
env["AGOLA_SKIPSSHHOSTKEYCHECK"] = "1"
}
annotations := map[string]string{
AnnotationProjectID: webhookData.ProjectID,
AnnotationRunType: string(runType),
AnnotationEventType: string(webhookData.Event),
AnnotationVirtualBranch: genAnnotationVirtualBranch(webhookData),
AnnotationCommitSHA: webhookData.CommitSHA,
AnnotationRef: webhookData.Ref,
AnnotationSender: webhookData.Sender,
AnnotationMessage: webhookData.Message,
AnnotationCommitLink: webhookData.CommitLink,
AnnotationCompareLink: webhookData.CompareLink,
}
if webhookData.Event == types.WebhookEventPush {
annotations[AnnotationBranch] = webhookData.Branch
annotations[AnnotationBranchLink] = webhookData.BranchLink
}
if webhookData.Event == types.WebhookEventTag {
annotations[AnnotationTag] = webhookData.Tag
annotations[AnnotationTagLink] = webhookData.TagLink
}
if webhookData.Event == types.WebhookEventPullRequest {
annotations[AnnotationPullRequestID] = webhookData.PullRequestID
annotations[AnnotationPullRequestLink] = webhookData.PullRequestLink
}
group := genGroup(webhookData.ProjectID, webhookData)
if err := h.createRuns(ctx, data, group, annotations, env); err != nil {
return http.StatusInternalServerError, "", errors.Wrapf(err, "failed to create run")
}
//if err := gitSource.CreateStatus(webhookData.Repo.Owner, webhookData.Repo.Name, webhookData.CommitSHA, gitsource.CommitStatusPending, "localhost:8080", "build %s", "agola"); err != nil {
// h.log.Errorf("failed to update commit status: %v", err)
//}
return 0, "", nil
}
func (h *webhooksHandler) createRuns(ctx context.Context, configData []byte, group string, annotations, env map[string]string) error {
config, err := config.ParseConfig([]byte(configData))
if err != nil {
return err
}
//h.log.Debugf("config: %v", util.Dump(config))
//h.log.Debugf("pipeline: %s", createRunOpts.PipelineName)
for _, pipeline := range config.Pipelines {
rc := runconfig.GenRunConfig(config, pipeline.Name, env)
h.log.Debugf("rc: %s", util.Dump(rc))
h.log.Infof("group: %s", group)
createRunReq := &rsapi.RunCreateRequest{
RunConfig: rc,
Group: group,
Annotations: annotations,
}
if _, err := h.runserviceClient.CreateRun(ctx, createRunReq); err != nil {
return err
}
}
return nil
}

View File

@ -18,57 +18,13 @@ import (
"time"
)
type WebhookEvent string
const (
WebhookEventPush WebhookEvent = "push"
WebhookEventTag WebhookEvent = "tag"
WebhookEventPullRequest WebhookEvent = "pull_request"
)
type RunType string
const (
RunTypeProject RunType = "project"
RunTypeUser RunType = "user"
)
type WebhookData struct {
Event WebhookEvent `json:"event,omitempty"`
ProjectID string `json:"project_id,omitempty"`
CompareLink string `json:"compare_link,omitempty"` // Pimray link to source. It can be the commit
CommitLink string `json:"commit_link,omitempty"` // Pimray link to source. It can be the commit
CommitSHA string `json:"commit_sha,omitempty"` // commit SHA (SHA1 but also future SHA like SHA256)
OldCommitSHA string `json:"old_commit_sha,omitempty"` // commit SHA of the head before this push
Ref string `json:"ref,omitempty"` // Ref containing the commit SHA
Message string `json:"message,omitempty"` // Message to use (Push last commit message summary, PR title, Tag message etc...)
Sender string `json:"sender,omitempty"`
Avatar string `json:"avatar,omitempty"`
Branch string `json:"branch,omitempty"`
BranchLink string `json:"branch_link,omitempty"`
Tag string `json:"tag,omitempty"`
TagLink string `json:"tag_link,omitempty"`
// use a string if on some platform (current or future) some PRs id will not be numbers
PullRequestID string `json:"pull_request_id,omitempty"`
PullRequestLink string `json:"link,omitempty"` // Link to pull request
Repo WebhookDataRepo `json:"repo,omitempty"`
}
type WebhookDataRepo struct {
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
FullName string `json:"full_name,omitempty"`
RepoURL string `json:"repo_url,omitempty"`
}
// Configstore types
type User struct {
// The type version. Increase when a breaking change is done. Usually not
// needed when adding fields.
Version string `json:"version,omitempty"`
ID string `json:"id,omitempty"`
UserName string `json:"user_name,omitempty"`
@ -97,6 +53,10 @@ const (
)
type RemoteSource struct {
// The type version. Increase when a breaking change is done. Usually not
// needed when adding fields.
Version string `json:"version,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
@ -113,6 +73,10 @@ type RemoteSource struct {
}
type LinkedAccount struct {
// The type version. Increase when a breaking change is done. Usually not
// needed when adding fields.
Version string `json:"version,omitempty"`
ID string `json:"id,omitempty"`
RemoteUserID string `json:"remote_user_id,omitempty"`
@ -128,6 +92,10 @@ type LinkedAccount struct {
}
type Project struct {
// The type version. Increase when a breaking change is done. Usually not
// needed when adding fields.
Version string `json:"version,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`

View File

@ -0,0 +1,63 @@
// 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 types
type WebhookEvent string
const (
WebhookEventPush WebhookEvent = "push"
WebhookEventTag WebhookEvent = "tag"
WebhookEventPullRequest WebhookEvent = "pull_request"
)
type RunType string
const (
RunTypeProject RunType = "project"
RunTypeUser RunType = "user"
)
type WebhookData struct {
Event WebhookEvent `json:"event,omitempty"`
ProjectID string `json:"project_id,omitempty"`
CompareLink string `json:"compare_link,omitempty"` // Pimray link to source. It can be the commit
CommitLink string `json:"commit_link,omitempty"` // Pimray link to source. It can be the commit
CommitSHA string `json:"commit_sha,omitempty"` // commit SHA (SHA1 but also future SHA like SHA256)
OldCommitSHA string `json:"old_commit_sha,omitempty"` // commit SHA of the head before this push
Ref string `json:"ref,omitempty"` // Ref containing the commit SHA
Message string `json:"message,omitempty"` // Message to use (Push last commit message summary, PR title, Tag message etc...)
Sender string `json:"sender,omitempty"`
Avatar string `json:"avatar,omitempty"`
Branch string `json:"branch,omitempty"`
BranchLink string `json:"branch_link,omitempty"`
Tag string `json:"tag,omitempty"`
TagLink string `json:"tag_link,omitempty"`
// use a string if on some platform (current or future) some PRs id will not be numbers
PullRequestID string `json:"pull_request_id,omitempty"`
PullRequestLink string `json:"link,omitempty"` // Link to pull request
Repo WebhookDataRepo `json:"repo,omitempty"`
}
type WebhookDataRepo struct {
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
FullName string `json:"full_name,omitempty"`
RepoURL string `json:"repo_url,omitempty"`
}

106
internal/util/backoff.go Normal file
View File

@ -0,0 +1,106 @@
// 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 util
import (
"errors"
"math/rand"
"time"
)
// DefaultRetry is the recommended retry for a conflict where multiple clients
// are making changes to the same resource.
var DefaultRetry = Backoff{
Steps: 5,
Duration: 10 * time.Millisecond,
Factor: 1.0,
Jitter: 0.1,
}
// DefaultBackoff is the recommended backoff for a conflict where a client
// may be attempting to make an unrelated modification to a resource under
// active management by one or more controllers.
var DefaultBackoff = Backoff{
Steps: 4,
Duration: 10 * time.Millisecond,
Factor: 5.0,
Jitter: 0.1,
}
// DefaultBackoff is the recommended backoff for a conflict where a client
// may be attempting to make an unrelated modification to a resource under
// active management by one or more controllers.
var FetchFileBackoff = Backoff{
Steps: 4,
Duration: 500 * time.Millisecond,
Factor: 2.0,
Jitter: 0.1,
}
// Jitter returns a time.Duration between duration and duration + maxFactor *
// duration.
//
// This allows clients to avoid converging on periodic behavior. If maxFactor
// is 0.0, a suggested default value will be chosen.
func Jitter(duration time.Duration, maxFactor float64) time.Duration {
if maxFactor <= 0.0 {
maxFactor = 1.0
}
wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
return wait
}
// ErrWaitTimeout is returned when the condition exited without success.
var ErrWaitTimeout = errors.New("timed out waiting for the condition")
// ConditionFunc returns true if the condition is satisfied, or an error
// if the loop should be aborted.
type ConditionFunc func() (done bool, err error)
// Backoff holds parameters applied to a Backoff function.
type Backoff struct {
Duration time.Duration // the base duration
Factor float64 // Duration is multiplied by factor each iteration
Jitter float64 // The amount of jitter applied each iteration
Steps int // Exit with error after this many steps
}
// ExponentialBackoff repeats a condition check with exponential backoff.
//
// It checks the condition up to Steps times, increasing the wait by multiplying
// the previous duration by Factor.
//
// If Jitter is greater than zero, a random amount of each duration is added
// (between duration and duration*(1+jitter)).
//
// If the condition never returns true, ErrWaitTimeout is returned. All other
// errors terminate immediately.
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
duration := backoff.Duration
for i := 0; i < backoff.Steps; i++ {
if i != 0 {
adjusted := duration
if backoff.Jitter > 0.0 {
adjusted = Jitter(duration, backoff.Jitter)
}
time.Sleep(adjusted)
duration = time.Duration(float64(duration) * backoff.Factor)
}
if ok, err := condition(); err != nil || ok {
return err
}
}
return ErrWaitTimeout
}

80
internal/util/ssh.go Normal file
View File

@ -0,0 +1,80 @@
// 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 util
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"golang.org/x/crypto/ssh"
)
// GenSSHKeyPair generate an ssh keypair in rsa format, returning the private
// key (in pem encoding) and the public key (in the OpenSSH base64 format)
func GenSSHKeyPair(bits int) ([]byte, []byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
err = privateKey.Validate()
if err != nil {
return nil, nil, err
}
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
var privateBuf bytes.Buffer
if err := pem.Encode(&privateBuf, privateKeyPEM); err != nil {
return nil, nil, errors.New("failed to pem encode private key")
}
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, errors.New("failed to generate public key")
}
// remove trailing \n returned by ssh.MarshalAuthorizedKey
return privateBuf.Bytes(), bytes.TrimSuffix(ssh.MarshalAuthorizedKey(pub), []byte("\n")), nil
}
// ExtraxtPublicKey extracts the public key from a ssh private key in pem format
func ExtractPublicKey(privateKeyPEM []byte) ([]byte, error) {
block, _ := pem.Decode(privateKeyPEM)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, errors.New("failed to decode PEM block containing rsa private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, errors.New("failed to parse rsa private key")
}
err = privateKey.Validate()
if err != nil {
return nil, errors.New("failed to validate rsa private key")
}
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, errors.New("failed to generate public key")
}
// remove trailing \n returned by ssh.MarshalAuthorizedKey
return bytes.TrimSuffix(ssh.MarshalAuthorizedKey(pub), []byte("\n")), nil
}

View File

@ -0,0 +1,30 @@
// 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 util
import (
"errors"
"regexp"
)
var nameRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*([-]?[a-zA-Z0-9]+)+$`)
var (
ErrValidation = errors.New("validation error")
)
func ValidateName(s string) bool {
return nameRegexp.MatchString(s)
}

View File

@ -0,0 +1,59 @@
// 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 util
import "testing"
var (
goodNames = []string{
"bar",
"foo-bar",
"foo-bar-baz",
"foo1",
"foo-1",
"foo-1-bar",
"f12oo-bar33",
}
badNames = []string{
"",
"foo bar",
" foo bar",
"foo bar ",
"-bar",
"bar-",
"-foo-bar",
"foo-bar-",
"foo--bar",
"foo.bar",
"foo_bar",
"foo#bar",
"1foobar",
}
)
func TestValidateName(t *testing.T) {
for _, name := range goodNames {
ok := ValidateName(name)
if !ok {
t.Errorf("expect valid name for %q", name)
}
}
for _, name := range badNames {
ok := ValidateName(name)
if ok {
t.Errorf("expect invalid name for %q", name)
}
}
}