gateway: initial implementation
This commit is contained in:
parent
4c0edd6374
commit
021a0465ce
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
10
go.mod
|
@ -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
15
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue