diff --git a/logging/filter.go b/logging/filter.go index 09e0c8ca..e17d7ea9 100644 --- a/logging/filter.go +++ b/logging/filter.go @@ -34,6 +34,9 @@ func Filter(logger *zap.SugaredLogger) func(req *restful.Request, resp *restful. req.Request = req.Request.WithContext(logging.WithLogger(req.Request.Context(), log)) chain.ProcessFilter(req, resp) if notHealthz(req.Request.URL.Path) { + if resp != nil { + log = log.With("statusCode", resp.StatusCode()) + } log.Debugw("<== returned a response") } } diff --git a/plugin/client/client_suite_test.go b/plugin/client/client_suite_test.go new file mode 100644 index 00000000..78b821c2 --- /dev/null +++ b/plugin/client/client_suite_test.go @@ -0,0 +1,13 @@ +package client + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestClient(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Client Suite") +} diff --git a/plugin/client/context.go b/plugin/client/context.go new file mode 100644 index 00000000..cea95839 --- /dev/null +++ b/plugin/client/context.go @@ -0,0 +1,40 @@ +/* +Copyright 2021 The Katanomi Authors. + +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 client + +import ( + "context" +) + +type pluginClientKey struct{} + +// WithPluginClient returns a copy of parent in which the pluginClient value is set +func WithPluginClient(parent context.Context, pluginClient *PluginClient) context.Context { + return context.WithValue(parent, pluginClientKey{}, pluginClient) +} + +// PluginClientFrom returns the value of the pluginClient key on the ctx +func PluginClientFrom(ctx context.Context) (*PluginClient, bool) { + pluginClient, ok := ctx.Value(pluginClientKey{}).(*PluginClient) + return pluginClient, ok +} + +// PluginClientValue returns the value of the pluginClient key on the ctx, or the nil if none +func PluginClientValue(ctx context.Context) *PluginClient { + pluginClient, _ := PluginClientFrom(ctx) + return pluginClient +} diff --git a/plugin/client/context_test.go b/plugin/client/context_test.go new file mode 100644 index 00000000..8f5b226f --- /dev/null +++ b/plugin/client/context_test.go @@ -0,0 +1,46 @@ +/* +Copyright 2022 The Katanomi Authors. + +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 client + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" +) + +func TestPluginClientContext(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + + defer ctrl.Finish() + + ctx := context.TODO() + + clt, ok := PluginClientFrom(ctx) + g.Expect(ok).To(BeFalse()) + g.Expect(clt).To(BeNil()) + + pluginClient := &PluginClient{} + ctx = WithPluginClient(ctx, pluginClient) + + g.Expect(PluginClientValue((ctx))).To(Equal(pluginClient)) + clt, ok = PluginClientFrom(ctx) + g.Expect(ok).To(BeTrue()) + g.Expect(clt).To(Equal(pluginClient)) +} diff --git a/plugin/client/git_plugin_client_test.go b/plugin/client/git_plugin_client_test.go new file mode 100644 index 00000000..b390123e --- /dev/null +++ b/plugin/client/git_plugin_client_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2022 The Katanomi Authors. + +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 client + +import ( + "context" + "reflect" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" + + metav1alpha1 "github.com/katanomi/pkg/apis/meta/v1alpha1" +) + +var _ = Describe("Test.GenerateGitPluginClient", func() { + var ( + ctx context.Context + secretRef *corev1.ObjectReference + gitRepoURL string + integrationClassName string + classAddress *duckv1.Addressable + params *PluginClient + err error + ) + BeforeEach(func() { + ctx = WithPluginClient(context.TODO(), NewPluginClient()) + secretRef = nil + gitRepoURL = "https://github.com/katanomi/pkg" + integrationClassName = "github" + classAddress = &duckv1.Addressable{} + classAddress.URL, _ = apis.ParseURL(gitRepoURL) + }) + + JustBeforeEach(func() { + params, err = GenerateGitPluginClient(ctx, secretRef, gitRepoURL, integrationClassName, classAddress) + }) + + Context("ctx without client and secretRef is not nil", func() { + BeforeEach(func() { + secretRef = &corev1.ObjectReference{ + Name: "name", + } + }) + It("should return error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + Context("invalid git repository url", func() { + BeforeEach(func() { + gitRepoURL = "http:// github.com" + }) + It("should return error", func() { + Expect(err).NotTo(BeNil()) + }) + }) + + Context("valid paramters", func() { + It("should generate success", func() { + Expect(err).To(BeNil()) + Expect(params).ToNot(BeNil()) + Expect(params.meta.BaseURL).To(Equal("https://github.com")) + Expect(params.GitRepo).To(Equal(metav1alpha1.GitRepo{ + Project: "katanomi", + Repository: "pkg", + })) + Expect(params.ClassAddress).To(Equal(classAddress)) + Expect(params.secret).To(Equal(corev1.Secret{})) + Expect(params.IntegrationClassName).To(Equal(integrationClassName)) + }) + }) +}) + +func TestGetGitRepoInfo(t *testing.T) { + tests := []struct { + name string + gitAddress string + wantHost string + wantGitRepo metav1alpha1.GitRepo + wantErr bool + }{ + { + name: "invalid git repo url", + gitAddress: "http:// github.com/katanomi/pkg.git", + wantHost: "", + wantGitRepo: metav1alpha1.GitRepo{}, + wantErr: true, + }, + { + name: "shuffix with .git", + gitAddress: "http://github.com/katanomi/pkg.git", + wantHost: "http://github.com", + wantGitRepo: metav1alpha1.GitRepo{ + Project: "katanomi", + Repository: "pkg", + }, + wantErr: false, + }, + { + name: "shuffix without .git", + gitAddress: "http://github.com/katanomi/pkg", + wantHost: "http://github.com", + wantGitRepo: metav1alpha1.GitRepo{ + Project: "katanomi", + Repository: "pkg", + }, + wantErr: false, + }, + { + name: "shuffix without /", + gitAddress: "http://github.com/katanomi/pkg/", + wantHost: "http://github.com", + wantGitRepo: metav1alpha1.GitRepo{ + Project: "katanomi", + Repository: "pkg", + }, + wantErr: false, + }, + { + name: "path too short", + gitAddress: "http://github.com/katanomi/", + wantHost: "", + wantGitRepo: metav1alpha1.GitRepo{}, + wantErr: true, + }, + { + name: "path with subgroup", + gitAddress: "http://github.com/katanomi/subgroup/pkg/", + wantHost: "http://github.com", + wantGitRepo: metav1alpha1.GitRepo{ + Project: "katanomi/subgroup", + Repository: "pkg", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotHost, gotGitRepo, err := GetGitRepoInfo(tt.gitAddress) + if (err != nil) != tt.wantErr { + t.Errorf("GetGitRepoInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotHost != tt.wantHost { + t.Errorf("GetGitRepoInfo() gotHost = %v, want %v", gotHost, tt.wantHost) + } + if !reflect.DeepEqual(gotGitRepo, tt.wantGitRepo) { + t.Errorf("GetGitRepoInfo() gotGitRepo = %v, want %v", gotGitRepo, tt.wantGitRepo) + } + }) + } +} diff --git a/plugin/client/git_pluginclient.go b/plugin/client/git_pluginclient.go new file mode 100644 index 00000000..9be01adc --- /dev/null +++ b/plugin/client/git_pluginclient.go @@ -0,0 +1,104 @@ +/* +Copyright 2022 The Katanomi Authors. + +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 client + +import ( + "context" + "fmt" + "net/url" + "strings" + + corev1 "k8s.io/api/core/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/logging" + + metav1alpha1 "github.com/katanomi/pkg/apis/meta/v1alpha1" + kclient "github.com/katanomi/pkg/client" + ksecret "github.com/katanomi/pkg/secret" +) + +// GenerateGitPluginClient generate git plugin client params +func GenerateGitPluginClient(ctx context.Context, secretRef *corev1.ObjectReference, + gitRepoURL, integrationClassName string, classAddress *duckv1.Addressable) ( + pclient *PluginClient, err error) { + + log := logging.FromContext(ctx) + pclient = PluginClientValue(ctx) + if pclient == nil { + pclient = NewPluginClient() + } else { + pclient = pclient.Clone() + } + + if secretRef != nil { + var secret *corev1.Secret + clt := kclient.Client(ctx) + if clt == nil { + err = fmt.Errorf("cannot get client from ctx") + return + } + secret, err = ksecret.GetSecretByRefOrLabel(ctx, clt, secretRef) + if err != nil { + err = fmt.Errorf("get secret for version steam failed: %w", err) + return + } + pclient = pclient.WithSecret(*secret) + } + + if integrationClassName != "" { + pclient = pclient.WithIntegrationClassName(integrationClassName) + } + if classAddress != nil { + pclient = pclient.WithClassAddress(classAddress) + } + + gitAddress, gitRepo, err := GetGitRepoInfo(gitRepoURL) + if err != nil { + err = fmt.Errorf("get git repo info failed: %w", err) + return + } + meta := Meta{ + BaseURL: gitAddress, + } + pclient = pclient. + WithMeta(meta). + WithGitRepo(gitRepo) + + log.Debugw("generate git plugin client", "BaseURL", gitAddress, "GitRepo", gitRepo, + "ClassAddress", classAddress, "IntegrationClassName", integrationClassName) + return pclient, nil +} + +// GetGitRepoInfo try to get the project and repository from the git address. +func GetGitRepoInfo(gitAddress string) (host string, gitRepo metav1alpha1.GitRepo, err error) { + gitAddress = strings.TrimSuffix(gitAddress, ".git") + URL, err := url.Parse(gitAddress) + if err != nil { + return + } + + projectRepo := strings.Split(strings.Trim(URL.Path, "/"), "/") + if len(projectRepo) < 2 { + err = fmt.Errorf("invaild git address %s which should have project and repository", gitAddress) + return + } + + host = fmt.Sprintf("%s://%s", URL.Scheme, URL.Host) + gitRepo.Project = strings.Join(projectRepo[:len(projectRepo)-1], "/") + gitRepo.Repository = projectRepo[len(projectRepo)-1] + return +} diff --git a/plugin/client/plugin_client.go b/plugin/client/plugin_client.go index 3ec90cfb..d492f4ae 100644 --- a/plugin/client/plugin_client.go +++ b/plugin/client/plugin_client.go @@ -22,14 +22,15 @@ import ( "strings" "github.com/go-resty/resty/v2" - "github.com/katanomi/pkg/client" - perrors "github.com/katanomi/pkg/errors" - "github.com/katanomi/pkg/tracing" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" duckv1 "knative.dev/pkg/apis/duck/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" metav1alpha1 "github.com/katanomi/pkg/apis/meta/v1alpha1" + "github.com/katanomi/pkg/client" + perrors "github.com/katanomi/pkg/errors" + "github.com/katanomi/pkg/tracing" ) var ( @@ -42,6 +43,30 @@ var ( // PluginClient client for plugins type PluginClient struct { client *resty.Client + + // meta plugin meta with base url and version info, for calling plugin api + // +optional + meta Meta + + // secret is the secret to use for the plugin client + // +optional + secret corev1.Secret + + // ClassAddress is the address of the integration class + // +optional + ClassAddress *duckv1.Addressable + + // ClassObject store the integration class object + // +optional + ClassObject ctrlclient.Object + + // IntegrationClassName is the name of the integration class + // +optional + IntegrationClassName string + + // GitRepo Repo base info, such as project, repository + // +optional + GitRepo metav1alpha1.GitRepo } // BuildOptions Options to build the plugin client @@ -77,6 +102,46 @@ var _ Client = &PluginClient{} // OptionFunc options for requests type OptionFunc func(request *resty.Request) +// Clone shallow clone the plugin client +// used to update some fields without changing the original +func (p *PluginClient) Clone() *PluginClient { + if p == nil { + return nil + } + p = &(*p) + return p +} + +func (p *PluginClient) WithMeta(meta Meta) *PluginClient { + p.meta = meta + return p +} + +func (p *PluginClient) WithSecret(secret corev1.Secret) *PluginClient { + p.secret = secret + return p +} + +func (p *PluginClient) WithClassAddress(classAddress *duckv1.Addressable) *PluginClient { + p.ClassAddress = classAddress + return p +} + +func (p *PluginClient) WithClassObject(object ctrlclient.Object) *PluginClient { + p.ClassObject = object + return p +} + +func (p *PluginClient) WithIntegrationClassName(integrationClassName string) *PluginClient { + p.IntegrationClassName = integrationClassName + return p +} + +func (p *PluginClient) WithGitRepo(gitRepo metav1alpha1.GitRepo) *PluginClient { + p.GitRepo = gitRepo + return p +} + // Get performs a GET request using defined options func (p *PluginClient) Get(ctx context.Context, baseURL *duckv1.Addressable, path string, options ...OptionFunc) error { options = append(defaultOptions, options...) @@ -191,72 +256,162 @@ func (p *PluginClient) Auth(meta Meta, secret corev1.Secret) ClientAuth { return newAuthClient(p, meta, secret) } +// NewAuth provides an auth methods for clients +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewAuth() ClientAuth { + return newAuthClient(p, p.meta, p.secret) +} + // Project get project client func (p *PluginClient) Project(meta Meta, secret corev1.Secret) ClientProject { return newProject(p, meta, secret) } +// NewProject get project client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewProject() ClientProject { + return newProject(p, p.meta, p.secret) +} + // Repository get Repository client func (p *PluginClient) Repository(meta Meta, secret corev1.Secret) ClientRepository { return newRepository(p, meta, secret) } +// NewRepository get Repository client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewRepository() ClientRepository { + return newRepository(p, p.meta, p.secret) +} + // Artifact get Artifact client func (p *PluginClient) Artifact(meta Meta, secret corev1.Secret) ClientArtifact { return newArtifact(p, meta, secret) } +// NewArtifact get Artifact client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewArtifact() ClientArtifact { + return newArtifact(p, p.meta, p.secret) +} + // GitBranch get branch client func (p *PluginClient) GitBranch(meta Meta, secret corev1.Secret) ClientGitBranch { return newGitBranch(p, meta, secret) } +// NewGitBranch get branch client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitBranch() ClientGitBranch { + return newGitBranch(p, p.meta, p.secret) +} + // GitContent get content client func (p *PluginClient) GitContent(meta Meta, secret corev1.Secret) ClientGitContent { return newGitContent(p, meta, secret) } +// NewGitContent get content client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitContent() ClientGitContent { + return newGitContent(p, p.meta, p.secret) +} + // GitPullRequest get pr client func (p *PluginClient) GitPullRequest(meta Meta, secret corev1.Secret) GitPullRequestCRUClient { return newGitPullRequest(p, meta, secret) } +// NewGitPullRequest get pr client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitPullRequest() GitPullRequestCRUClient { + return newGitPullRequest(p, p.meta, p.secret) +} + // GitCommit get pr client func (p *PluginClient) GitCommit(meta Meta, secret corev1.Secret) ClientGitCommit { return newGitCommit(p, meta, secret) } +// NewGitCommit get pr client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitCommit() ClientGitCommit { + return newGitCommit(p, p.meta, p.secret) +} + // GitRepository get repo client func (p *PluginClient) GitRepository(meta Meta, secret corev1.Secret) ClientGitRepository { return newGitRepository(p, meta, secret) } +// NewGitRepository get repo client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitRepository() ClientGitRepository { + return newGitRepository(p, p.meta, p.secret) +} + // GitRepositoryFileTree get repo file tree client func (p *PluginClient) GitRepositoryFileTree(meta Meta, secret corev1.Secret) ClientGitRepositoryFileTree { return newGitRepositoryFileTree(p, meta, secret) } +// NewGitRepositoryFileTree get repo file tree client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitRepositoryFileTree() ClientGitRepositoryFileTree { + return newGitRepositoryFileTree(p, p.meta, p.secret) +} + // GitCommitComment get commit comment client func (p *PluginClient) GitCommitComment(meta Meta, secret corev1.Secret) ClientGitCommitComment { return newGitCommitComment(p, meta, secret) } +// NewGitCommitComment get commit comment client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitCommitComment() ClientGitCommitComment { + return newGitCommitComment(p, p.meta, p.secret) +} + // GitCommitStatus get commit comment client func (p *PluginClient) GitCommitStatus(meta Meta, secret corev1.Secret) ClientGitCommitStatus { return newGitCommitStatus(p, meta, secret) } +// NewGitCommitStatus get commit comment client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitCommitStatus() ClientGitCommitStatus { + return newGitCommitStatus(p, p.meta, p.secret) +} + // CodeQuality get code quality client func (p *PluginClient) CodeQuality(meta Meta, secret corev1.Secret) ClientCodeQuality { return newCodeQuality(p, meta, secret) } +// NewCodeQuality get code quality client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewCodeQuality() ClientCodeQuality { + return newCodeQuality(p, p.meta, p.secret) +} + // BlobStore get blob store client func (p *PluginClient) BlobStore(meta Meta, secret corev1.Secret) ClientBlobStore { return newBlobStore(p, meta, secret) } +// NewBlobStore get blob store client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewBlobStore() ClientBlobStore { + return newBlobStore(p, p.meta, p.secret) +} + // GitRepositoryTag get repository tag client func (p *PluginClient) GitRepositoryTag(meta Meta, secret corev1.Secret) ClientGitRepositoryTag { return newGitRepositoryTag(p, meta, secret) } + +// NewGitRepositoryTag get repository tag client +// Use the internal meta and secret to generate the client, please assign in advance. +func (p *PluginClient) NewGitRepositoryTag() ClientGitRepositoryTag { + return newGitRepositoryTag(p, p.meta, p.secret) +}