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 logger = slog.New(level)
|
||||||
var log = logger.Sugar()
|
var log = logger.Sugar()
|
||||||
|
|
||||||
|
var token string
|
||||||
|
|
||||||
var cmdAgola = &cobra.Command{
|
var cmdAgola = &cobra.Command{
|
||||||
Use: "agola",
|
Use: "agola",
|
||||||
Short: "agola",
|
Short: "agola",
|
||||||
|
@ -68,6 +70,7 @@ func init() {
|
||||||
flags := cmdAgola.PersistentFlags()
|
flags := cmdAgola.PersistentFlags()
|
||||||
|
|
||||||
flags.StringVarP(&agolaOpts.gatewayURL, "gateway-url", "u", gatewayURL, "agola gateway exposed url")
|
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")
|
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/cmd"
|
||||||
"github.com/sorintlab/agola/internal/services/config"
|
"github.com/sorintlab/agola/internal/services/config"
|
||||||
"github.com/sorintlab/agola/internal/services/configstore"
|
"github.com/sorintlab/agola/internal/services/configstore"
|
||||||
|
"github.com/sorintlab/agola/internal/services/gateway"
|
||||||
"github.com/sorintlab/agola/internal/services/runservice/executor"
|
"github.com/sorintlab/agola/internal/services/runservice/executor"
|
||||||
rsscheduler "github.com/sorintlab/agola/internal/services/runservice/scheduler"
|
rsscheduler "github.com/sorintlab/agola/internal/services/runservice/scheduler"
|
||||||
"github.com/sorintlab/agola/internal/services/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")
|
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)
|
errCh := make(chan error)
|
||||||
|
|
||||||
go func() { errCh <- rsex1.Run(ctx) }()
|
go func() { errCh <- rsex1.Run(ctx) }()
|
||||||
go func() { errCh <- rssched1.Run(ctx) }()
|
go func() { errCh <- rssched1.Run(ctx) }()
|
||||||
go func() { errCh <- cs.Run(ctx) }()
|
go func() { errCh <- cs.Run(ctx) }()
|
||||||
|
go func() { errCh <- gateway.Run(ctx) }()
|
||||||
go func() { errCh <- sched1.Run(ctx) }()
|
go func() { errCh <- sched1.Run(ctx) }()
|
||||||
|
|
||||||
return <-errCh
|
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
|
module github.com/sorintlab/agola
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
code.gitea.io/sdk v0.0.0-20190219191342-62c4fab696b4
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Masterminds/squirrel v0.0.0-20181204161840-e5bf00f96d4a
|
github.com/Masterminds/squirrel v0.0.0-20181204161840-e5bf00f96d4a
|
||||||
github.com/Microsoft/go-winio v0.4.11 // indirect
|
github.com/Microsoft/go-winio v0.4.11 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/bmatcuk/doublestar v1.1.1
|
github.com/bmatcuk/doublestar v1.1.1
|
||||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
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/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v1.13.1
|
github.com/docker/docker v1.13.1
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.3.3 // 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-bindata/go-bindata v1.0.0
|
||||||
github.com/go-ini/ini v1.42.0 // indirect
|
github.com/go-ini/ini v1.42.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.4.1 // 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/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||||
github.com/spf13/cobra v0.0.3
|
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.etcd.io/etcd v0.0.0-20181128220305-dedae6eb7c25
|
||||||
go.uber.org/zap v1.9.1
|
go.uber.org/zap v1.9.1
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e // indirect
|
golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
|
||||||
google.golang.org/appengine v1.4.0 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.41.0 // indirect
|
gopkg.in/ini.v1 v1.41.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
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 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
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=
|
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/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 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
|
||||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
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/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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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/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 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
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 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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=
|
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 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
|
||||||
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
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/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 h1:MPPkRncZLN9Kh4MEFmbnK4h3BD7AUmskWv2+EeZJCCs=
|
||||||
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
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=
|
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/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-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-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 h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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=
|
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/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 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
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=
|
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"
|
"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
|
// Configstore types
|
||||||
|
|
||||||
type User struct {
|
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"`
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
UserName string `json:"user_name,omitempty"`
|
UserName string `json:"user_name,omitempty"`
|
||||||
|
@ -97,6 +53,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type RemoteSource struct {
|
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"`
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
@ -113,6 +73,10 @@ type RemoteSource struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LinkedAccount 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"`
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
RemoteUserID string `json:"remote_user_id,omitempty"`
|
RemoteUserID string `json:"remote_user_id,omitempty"`
|
||||||
|
@ -128,6 +92,10 @@ type LinkedAccount struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Project 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"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name,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