diff --git a/Makefile b/Makefile index 30a2ef1..f544f40 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,9 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." +goreleaser: + goreleaser release --rm-dist --snapshot + fmt: ## Run go fmt against code. go fmt ./... @@ -86,7 +89,8 @@ deploy-without-update: undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/default | kubectl delete -f - - +undeploy-without-update: + kustomize build config/default | kubectl delete -f - CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. diff --git a/api/v1alpha1/releaser_types.go b/api/v1alpha1/releaser_types.go index 3fe657b..869e77c 100644 --- a/api/v1alpha1/releaser_types.go +++ b/api/v1alpha1/releaser_types.go @@ -53,7 +53,7 @@ const ( func (p *Phase) IsValid() bool { switch *p { case PhaseDraft, PhaseReady, PhaseDone: - return true + return true default: return false } @@ -80,11 +80,11 @@ type GitOps struct { type Provider string const ( - ProviderGitHub Provider = "github" - ProviderGitlab Provider = "gitlab" + ProviderGitHub Provider = "github" + ProviderGitlab Provider = "gitlab" ProviderBitbucket Provider = "bitbucket" - ProviderGitee Provider = "gitee" - ProviderUnknown Provider = "unknown" + ProviderGitee Provider = "gitee" + ProviderUnknown Provider = "unknown" ) // Action indicates the action once the request phase to be ready @@ -107,21 +107,37 @@ type ReleaserStatus struct { // Condition indicates the status of each git repositories type Condition struct { - RepositoryName string `json:"repositoryName"` - Status string `json:"status"` - Message string `json:"message"` + ConditionType ConditionType `json:"conditionType"` + Status ConditionStatus `json:"status"` + Message string `json:"message"` } +// ConditionType is the type of a condition +type ConditionType string + +const ( + ConditionTypeRelease ConditionType = "release" + ConditionTypeOther ConditionType = "other" +) + +// ConditionStatus is the status of a condition +type ConditionStatus string + +const ( + ConditionStatusSuccess ConditionStatus = "success" + ConditionStatusFailed ConditionStatus = "failed" +) + //+kubebuilder:object:root=true //+kubebuilder:subresource:status // Releaser is the Schema for the releasers API type Releaser struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ReleaserSpec `json:"spec,omitempty"` + Spec ReleaserSpec `json:"spec,omitempty"` // +optional Status ReleaserStatus `json:"status,omitempty"` } diff --git a/config/crd/bases/devops.kubesphere.io_releasers.yaml b/config/crd/bases/devops.kubesphere.io_releasers.yaml index 919ce56..d19c776 100644 --- a/config/crd/bases/devops.kubesphere.io_releasers.yaml +++ b/config/crd/bases/devops.kubesphere.io_releasers.yaml @@ -123,15 +123,17 @@ spec: items: description: Condition indicates the status of each git repositories properties: - message: + conditionType: + description: ConditionType is the type of a condition type: string - repositoryName: + message: type: string status: + description: ConditionStatus is the status of a condition type: string required: + - conditionType - message - - repositoryName - status type: object type: array diff --git a/config/samples/devops_v1alpha1_releaser.yaml b/config/samples/devops_v1alpha1_releaser.yaml index f90cb0b..c6e7fa2 100644 --- a/config/samples/devops_v1alpha1_releaser.yaml +++ b/config/samples/devops_v1alpha1_releaser.yaml @@ -1,7 +1,7 @@ apiVersion: devops.kubesphere.io/v1alpha1 kind: Releaser metadata: - name: releaser-sample-4 + name: releaser-sample-v0.0.9 spec: version: v0.0.4 gitOps: @@ -15,7 +15,7 @@ spec: address: https://github.com/linuxsuren-bot/test action: release branch: master - version: v0.0.4 + version: v0.0.8 provider: github secret: name: test-git diff --git a/controllers/git.go b/controllers/git.go new file mode 100644 index 0000000..66d185d --- /dev/null +++ b/controllers/git.go @@ -0,0 +1,240 @@ +package controllers + +import ( + "errors" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/go-git/go-git/v5/plumbing/transport" + devopsv1alpha1 "github.com/kubesphere-sigs/ks-releaser/api/v1alpha1" + "github.com/kubesphere-sigs/ks-releaser/controllers/internal_scm" + "golang.org/x/crypto/ssh" + githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" + "io/ioutil" + v1 "k8s.io/api/core/v1" + "net/url" + "os" + "path" + "strings" + "time" +) + +/** +TODO make these functions into a struct +For example, we can share parts of the variables, such as git.Repository, secret .etc. + */ + +func saveAndPush(gitRepo *git.Repository, user, targetFile string, data []byte, secret *v1.Secret) (err error) { + if err = ioutil.WriteFile(targetFile, data, 0644); err != nil { + fmt.Println("failed to write file", targetFile) + } else { + if err = addAndCommit(gitRepo, user); err == nil { + err = pushTags(gitRepo, "", getAuth(secret)) + } + } + return +} + +func addAndCommit(repo *git.Repository, user string) (err error) { + var w *git.Worktree + if w, err = repo.Worktree(); err == nil { + _, _ = w.Add(".") + var commit plumbing.Hash + commit, err = w.Commit("example go-git commit", &git.CommitOptions{ + All: true, + Author: &object.Signature{ + Name: user, + Email: fmt.Sprintf("%s@users.noreply.github.com", user), + When: time.Now(), + }, + }) + + if err == nil { + _, err = repo.CommitObject(commit) + } + } + return +} + +func release(repo devopsv1alpha1.Repository, secret *v1.Secret, user string) (err error) { + auth := getAuth(secret) + + var gitRepo *git.Repository + if gitRepo, err = clone(repo.Address, repo.Branch, auth, "tmp"); err != nil { + return + } + + if repo.Message == "" { + repo.Message = "released by ks-releaser" + } + if _, err = setTag(gitRepo, repo.Version, repo.Message, user); err != nil { + return + } + + if err = pushTags(gitRepo, repo.Version, auth); err != nil { + return + } + + var orgAndRepo string + switch repo.Provider { + case devopsv1alpha1.ProviderGitHub: + orgAndRepo = strings.ReplaceAll(repo.Address, "https://github.com/", "") + } + + switch repo.Action { + case devopsv1alpha1.ActionPreRelease: + provider := internal_scm.GetGitProvider(string(repo.Provider), orgAndRepo, string(secret.Data[v1.BasicAuthPasswordKey])) + if provider != nil { + err = provider.Release(repo.Version, false, true) + } + case devopsv1alpha1.ActionRelease: + provider := internal_scm.GetGitProvider(string(repo.Provider), orgAndRepo, string(secret.Data[v1.BasicAuthPasswordKey])) + if provider != nil { + err = provider.Release(repo.Version, false, false) + } + } + return +} + +func getAuth(secret *v1.Secret) (auth transport.AuthMethod) { + switch secret.Type { + case v1.SecretTypeBasicAuth: + auth = &githttp.BasicAuth{ + Username: string(secret.Data[v1.BasicAuthUsernameKey]), + Password: string(secret.Data[v1.BasicAuthPasswordKey]), + } + case v1.SecretTypeSSHAuth: + signer, _ := ssh.ParsePrivateKey(secret.Data[v1.SSHAuthPrivateKey]) + auth = &gitssh.PublicKeys{User: "git", Signer: signer} + } + return +} + +func clone(gitRepo, branch string, auth transport.AuthMethod, cacheDir string) (repo *git.Repository, err error) { + var gitRepoURL *url.URL + if gitRepoURL, err = url.Parse(gitRepo); err != nil { + return + } + + dir := path.Join(cacheDir, gitRepoURL.Path) + if ok, _ := PathExists(dir); ok { + if repo, err = git.PlainOpen(dir); err == nil { + var wd *git.Worktree + + if wd, err = repo.Worktree(); err == nil { + if err = wd.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branch), + Create: false, + Force: true, + }); err != nil { + err = fmt.Errorf("unable to checkout git branch: %s", branch) + return + } + + if err = wd.Pull(&git.PullOptions{ + Progress: os.Stdout, + ReferenceName: plumbing.NewBranchReferenceName(branch), + Force: true, // in case of the force pushing + Auth: auth, + }); err != nil && err != git.NoErrAlreadyUpToDate { + err = fmt.Errorf("failed to pull git repository '%s', error: %v", repo, err) + } else { + err = nil + } + } + } else { + err = fmt.Errorf("failed to open git local repository, error: %v", err) + } + } else { + repo, err = git.PlainClone(dir, false, &git.CloneOptions{ + URL: gitRepo, + ReferenceName: plumbing.NewBranchReferenceName(branch), + Progress: os.Stdout, + Auth: auth, + }) + } + return +} + +func tagExists(tag string, r *git.Repository) bool { + var err error + var tags storer.ReferenceIter + if tags, err = r.Tags(); err == nil { + err = tags.ForEach(func(reference *plumbing.Reference) error { + tagRef := reference.Name() + if tagRef.IsTag() && !tagRef.IsRemote() { + _ = r.DeleteTag(tag) + } else if tagRef.IsTag() && tagRef.IsRemote() && tagRef.String() == tag { + return nil + } + return errors.New("not found tag") + }) + } + return err == nil || (err != nil && err.Error() != "not found tag") +} + +func setTag(r *git.Repository, tag, message, user string) (bool, error) { + if tagExists(tag, r) { + fmt.Printf("tag %s already exists\n", tag) + return false, nil + } + fmt.Printf("Set tag %s\n", tag) + h, err := r.Head() + if err != nil { + fmt.Printf("get HEAD error: %s\n", err) + return false, err + } + _, err = r.CreateTag(tag, h.Hash(), &git.CreateTagOptions{ + Tagger: &object.Signature{ + Name: user, + Email: fmt.Sprintf("%s@users.noreply.github.com", user), + When: time.Time{}, + }, + Message: message, + }) + + if err != nil { + fmt.Printf("create tag error: %s\n", err) + return false, err + } + return true, nil +} + +func pushTags(r *git.Repository, tag string, auth transport.AuthMethod) (err error) { + var ref []config.RefSpec + if tag != "" { + ref = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tag, tag))} + } + + po := &git.PushOptions{ + RemoteName: "origin", + Progress: os.Stdout, + RefSpecs: ref, + Auth: auth, + } + if err = r.Push(po); err != nil { + if err == git.NoErrAlreadyUpToDate { + fmt.Print("origin remote was up to date, no push done\n") + err = nil + return + } + err = fmt.Errorf("push to remote origin error: %s\n", err) + } + return +} + +// PathExists checks if the target path exist or not +func PathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/controllers/internal_scm/factory.go b/controllers/internal_scm/factory.go index e235a0f..1c14bdb 100644 --- a/controllers/internal_scm/factory.go +++ b/controllers/internal_scm/factory.go @@ -6,6 +6,7 @@ type GitReleaser interface { Release(version string, draft, prerelease bool) (err error) } +// GetGitProvider returns the GitReleaser implement by kind func GetGitProvider(kind, repo, token string) GitReleaser { switch v1alpha1.Provider(kind) { case v1alpha1.ProviderGitHub: diff --git a/controllers/internal_scm/factory_test.go b/controllers/internal_scm/factory_test.go new file mode 100644 index 0000000..df5e5c9 --- /dev/null +++ b/controllers/internal_scm/factory_test.go @@ -0,0 +1,39 @@ +package internal_scm + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetGitProvider(t *testing.T) { + type args struct { + kind string + repo string + token string + } + tests := []struct { + name string + args args + exist bool + }{{ + name: "github", + args: args{ + kind: "github", + }, + exist: true, + }, { + name: "fake", + args: args{}, + exist: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetGitProvider(tt.args.kind, tt.args.repo, tt.args.token) + if tt.exist { + assert.NotNil(t, got) + } else { + assert.Nil(t, got) + } + }) + } +} diff --git a/controllers/releaser_controller.go b/controllers/releaser_controller.go index 1c011f3..ce43921 100644 --- a/controllers/releaser_controller.go +++ b/controllers/releaser_controller.go @@ -18,26 +18,15 @@ package controllers import ( "context" - "errors" "fmt" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/storer" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/kubesphere-sigs/ks-releaser/controllers/internal_scm" - "golang.org/x/crypto/ssh" - githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" - gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" + "github.com/go-logr/logr" "io/ioutil" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "net/url" - "os" "path" - "strings" "time" "k8s.io/apimachinery/pkg/runtime" @@ -50,8 +39,10 @@ import ( // ReleaserReconciler reconciles a Releaser object type ReleaserReconciler struct { + logger logr.Logger client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + GitCacheDir string gitUser string } @@ -63,7 +54,6 @@ type ReleaserReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by // the Releaser object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. @@ -71,7 +61,7 @@ type ReleaserReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile func (r *ReleaserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { - _ = log.FromContext(ctx) + r.logger = log.FromContext(ctx) releaser := &devopsv1alpha1.Releaser{} if err = r.Get(ctx, req.NamespacedName, releaser); err != nil { @@ -87,10 +77,7 @@ func (r *ReleaserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r return } - //if err = r.Get(ctx, req.NamespacedName, releaser); err != nil { - // err = client.IgnoreNotFound(err) - // return - //} + r.logger.Info("start to release", "name", releaser.Name) secret := &v1.Secret{} if err = r.Get(ctx, types.NamespacedName{ Namespace: spec.Secret.Namespace, @@ -99,6 +86,7 @@ func (r *ReleaserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r return } + releaser.Status.Conditions = make([]devopsv1alpha1.Condition, 0) r.gitUser = string(secret.Data[v1.BasicAuthUsernameKey]) var errSlice = ErrorSlice{} @@ -108,16 +96,16 @@ func (r *ReleaserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r var condition devopsv1alpha1.Condition if releaseRrr == nil { condition = devopsv1alpha1.Condition{ - RepositoryName: repo.Name, - Status: "success", - Message: "success", + ConditionType: devopsv1alpha1.ConditionTypeRelease, + Status: devopsv1alpha1.ConditionStatusSuccess, + Message: fmt.Sprintf("%s was released", repo.Address), } } else { errSlice = errSlice.append(releaseRrr) condition = devopsv1alpha1.Condition{ - RepositoryName: repo.Name, - Status: "failed", - Message: releaseRrr.Error(), + ConditionType: devopsv1alpha1.ConditionTypeRelease, + Status: devopsv1alpha1.ConditionStatusFailed, + Message: fmt.Sprintf("failed to release %s, error: %v", repo.Address, releaseRrr.Error()), } } addCondition(releaser, condition) @@ -131,31 +119,25 @@ func (r *ReleaserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r } } - r.markAsDone(secret, releaser) - if updateErr := r.Status().Update(ctx, releaser); updateErr == nil { + if err == nil { + if err = r.markAsDone(secret, releaser); err != nil { + condition := devopsv1alpha1.Condition{ + ConditionType: devopsv1alpha1.ConditionTypeOther, + Status: devopsv1alpha1.ConditionStatusFailed, + Message: fmt.Sprintf("failed to mark as done: %v", err), + } + addCondition(releaser, condition) + } + } + + if updateErr := r.Status().Update(ctx, releaser); err == nil && updateErr == nil { r.updateHash(ctx, releaser) } else { - fmt.Println(updateErr) + err = updateErr } return } -// addCondition adds or replaces a condition -func addCondition(releaser *devopsv1alpha1.Releaser, condition devopsv1alpha1.Condition) { - if condition.RepositoryName == "" { - return - } - - for i, _ := range releaser.Status.Conditions { - item := releaser.Status.Conditions[i] - if item.RepositoryName == condition.RepositoryName { - releaser.Status.Conditions[i] = condition - return - } - } - releaser.Status.Conditions = append(releaser.Status.Conditions, condition) -} - func (r *ReleaserReconciler) needToUpdate(ctx context.Context, releaser *devopsv1alpha1.Releaser) bool { hash := releaser.Annotations["releaser.devops.kubesphere.io/hash"] newHash := ComputeHash(releaser.Spec) @@ -172,185 +154,6 @@ func (r *ReleaserReconciler) updateHash(ctx context.Context, releaser *devopsv1a _ = r.Update(ctx, releaser) } -func release(repo devopsv1alpha1.Repository, secret *v1.Secret, user string) (err error) { - auth := getAuth(secret) - - var gitRepo *git.Repository - if gitRepo, err = clone(repo.Address, repo.Branch, auth, "tmp"); err != nil { - return - } - - if repo.Message == "" { - repo.Message = "released by ks-releaser" - } - if _, err = setTag(gitRepo, repo.Version, repo.Message, user); err != nil { - return - } - - if err = pushTags(gitRepo, repo.Version, auth); err != nil { - return - } - - var orgAndRepo string - switch repo.Provider { - case devopsv1alpha1.ProviderGitHub: - orgAndRepo = strings.ReplaceAll(repo.Address, "https://github.com/", "") - } - - switch repo.Action { - case devopsv1alpha1.ActionPreRelease: - provider := internal_scm.GetGitProvider(string(repo.Provider), orgAndRepo, string(secret.Data[v1.BasicAuthPasswordKey])) - if provider != nil { - err = provider.Release(repo.Version, false, true) - } - case devopsv1alpha1.ActionRelease: - provider := internal_scm.GetGitProvider(string(repo.Provider), orgAndRepo, string(secret.Data[v1.BasicAuthPasswordKey])) - if provider != nil { - err = provider.Release(repo.Version, false, false) - } - } - return -} - -func getAuth(secret *v1.Secret) (auth transport.AuthMethod) { - switch secret.Type { - case v1.SecretTypeBasicAuth: - auth = &githttp.BasicAuth{ - Username: string(secret.Data[v1.BasicAuthUsernameKey]), - Password: string(secret.Data[v1.BasicAuthPasswordKey]), - } - case v1.SecretTypeSSHAuth: - signer, _ := ssh.ParsePrivateKey(secret.Data[v1.SSHAuthPrivateKey]) - auth = &gitssh.PublicKeys{User: "git", Signer: signer} - } - return -} - -func clone(gitRepo, branch string, auth transport.AuthMethod, cacheDir string) (repo *git.Repository, err error) { - var gitRepoURL *url.URL - if gitRepoURL, err = url.Parse(gitRepo); err != nil { - return - } - - dir := path.Join(cacheDir, gitRepoURL.Path) - if ok, _ := PathExists(dir); ok { - if repo, err = git.PlainOpen(dir); err == nil { - var wd *git.Worktree - - if wd, err = repo.Worktree(); err == nil { - if err = wd.Checkout(&git.CheckoutOptions{ - Branch: plumbing.NewBranchReferenceName(branch), - Create: false, - Force: true, - }); err != nil { - err = fmt.Errorf("unable to checkout git branch: %s", branch) - return - } - - if err = wd.Pull(&git.PullOptions{ - Progress: os.Stdout, - ReferenceName: plumbing.NewBranchReferenceName(branch), - Force: true, // in case of the force pushing - Auth: auth, - }); err != nil && err != git.NoErrAlreadyUpToDate { - err = fmt.Errorf("failed to pull git repository '%s', error: %v", repo, err) - } else { - err = nil - } - } - } else { - err = fmt.Errorf("failed to open git local repository, error: %v", err) - } - } else { - repo, err = git.PlainClone(dir, false, &git.CloneOptions{ - URL: gitRepo, - ReferenceName: plumbing.NewBranchReferenceName(branch), - Progress: os.Stdout, - Auth: auth, - }) - } - return -} - -func tagExists(tag string, r *git.Repository) bool { - var err error - var tags storer.ReferenceIter - if tags, err = r.Tags(); err == nil { - err = tags.ForEach(func(reference *plumbing.Reference) error { - tagRef := reference.Name() - if tagRef.IsTag() && !tagRef.IsRemote() { - _ = r.DeleteTag(tag) - } else if tagRef.IsTag() && tagRef.IsRemote() && tagRef.String() == tag { - return nil - } - return errors.New("not found tag") - }) - } - return err == nil || (err != nil && err.Error() != "not found tag") -} - -func setTag(r *git.Repository, tag, message, user string) (bool, error) { - if tagExists(tag, r) { - fmt.Printf("tag %s already exists\n", tag) - return false, nil - } - fmt.Printf("Set tag %s\n", tag) - h, err := r.Head() - if err != nil { - fmt.Printf("get HEAD error: %s\n", err) - return false, err - } - _, err = r.CreateTag(tag, h.Hash(), &git.CreateTagOptions{ - Tagger: &object.Signature{ - Name: user, - Email: fmt.Sprintf("%s@users.noreply.github.com", user), - When: time.Time{}, - }, - Message: message, - }) - - if err != nil { - fmt.Printf("create tag error: %s\n", err) - return false, err - } - return true, nil -} - -func pushTags(r *git.Repository, tag string, auth transport.AuthMethod) (err error) { - var ref []config.RefSpec - if tag != "" { - ref = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", tag, tag))} - } - - po := &git.PushOptions{ - RemoteName: "origin", - Progress: os.Stdout, - RefSpecs: ref, - Auth: auth, - } - if err = r.Push(po); err != nil { - if err == git.NoErrAlreadyUpToDate { - fmt.Print("origin remote was up to date, no push done\n") - err = nil - return - } - err = fmt.Errorf("push to remote origin error: %s\n", err) - } - return -} - -// PathExists checks if the target path exist or not -func PathExists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} - // SetupWithManager sets up the controller with the Manager. func (r *ReleaserReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -358,7 +161,7 @@ func (r *ReleaserReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *ReleaserReconciler) markAsDone(secret *v1.Secret, releaser *devopsv1alpha1.Releaser) { +func (r *ReleaserReconciler) markAsDone(secret *v1.Secret, releaser *devopsv1alpha1.Releaser) (err error) { gitOps := releaser.Spec.GitOps if gitOps == nil || !gitOps.Enable { releaser.Spec.Phase = devopsv1alpha1.PhaseDone @@ -366,59 +169,46 @@ func (r *ReleaserReconciler) markAsDone(secret *v1.Secret, releaser *devopsv1alp return } - var err error var gitRepo *git.Repository repo := gitOps.Repository - if gitRepo, err = clone(repo.Address, repo.Branch, getAuth(secret), "tmp"); err == nil { - var gitRepoURL *url.URL - if gitRepoURL, err = url.Parse(repo.Address); err != nil { - return - } + if gitRepo, err = clone(repo.Address, repo.Branch, getAuth(secret), r.GitCacheDir); err != nil { + err = fmt.Errorf("failed to clone repository: %s, error: %v", repo.Address, err) + return + } - dir := path.Join("tmp", gitRepoURL.Path) - filePath := path.Join(dir, fmt.Sprintf("%s.yaml", releaser.Name)) + var gitRepoURL *url.URL + if gitRepoURL, err = url.Parse(repo.Address); err != nil { + return + } + + dir := path.Join(r.GitCacheDir, gitRepoURL.Path) + filePath := path.Join(dir, fmt.Sprintf("%s.yaml", releaser.Name)) + + var data []byte + if data, err = ioutil.ReadFile(filePath); err == nil { + data, err = updateReleaserAsYAML(data, func(releaser *devopsv1alpha1.Releaser) { + releaser.Spec.Phase = devopsv1alpha1.PhaseDone + }) + if err == nil { + if err = saveAndPush(gitRepo, r.gitUser, filePath, data, secret); err != nil { + fmt.Println("failed to write file", filePath) + } - var data []byte - if data, err = ioutil.ReadFile(filePath); err == nil { - data, err = updateReleaserAsYAML(data, func(releaser *devopsv1alpha1.Releaser) { - releaser.Spec.Phase = devopsv1alpha1.PhaseDone - }) if err == nil { - if err = ioutil.WriteFile(filePath, data, 0644); err != nil { - fmt.Println("failed to write file", filePath) + var bumpFilename string + if data, bumpFilename, err = bumpReleaserAsData(data); err != nil { + err = fmt.Errorf("failed to bump releaser: %s, error: %v", filePath, err) } else { - if err = addAndCommit(gitRepo, r.gitUser); err == nil { - err = pushTags(gitRepo, "", getAuth(secret)) - } + bumpFilename = path.Join(dir, bumpFilename) + err = saveAndPush(gitRepo, r.gitUser, bumpFilename, data, secret) } } } - - if err != nil { - fmt.Println(err) - } - } else { - fmt.Println("failed to clone gitops repo", err) } + return } -func addAndCommit(repo *git.Repository, user string) (err error) { - var w *git.Worktree - if w, err = repo.Worktree(); err == nil { - _, _ = w.Add(".") - var commit plumbing.Hash - commit, err = w.Commit("example go-git commit", &git.CommitOptions{ - All: true, - Author: &object.Signature{ - Name: user, - Email: fmt.Sprintf("%s@users.noreply.github.com", user), - When: time.Now(), - }, - }) - - if err == nil { - _, err = repo.CommitObject(commit) - } - } - return +// addCondition adds or replaces a condition +func addCondition(releaser *devopsv1alpha1.Releaser, condition devopsv1alpha1.Condition) { + releaser.Status.Conditions = append(releaser.Status.Conditions, condition) } diff --git a/controllers/version.go b/controllers/version.go new file mode 100644 index 0000000..f650a38 --- /dev/null +++ b/controllers/version.go @@ -0,0 +1,54 @@ +package controllers + +import ( + "fmt" + "github.com/blang/semver" + devopsv1alpha1 "github.com/kubesphere-sigs/ks-releaser/api/v1alpha1" + "sigs.k8s.io/yaml" + "strings" +) + +func bumpVersion(versionStr string) (nextVersion string, err error) { + nextVersion = versionStr // keep using the old version if there's any problem happened + + var version semver.Version + if version, err = semver.ParseTolerant(versionStr); err != nil { + err = fmt.Errorf("cannot bump an invalid version: %s, error: %v", versionStr, err) + return + } + + version.Patch += 1 + nextVersion = version.String() + if strings.HasPrefix(versionStr, "v") { + nextVersion = "v" + nextVersion + } + return +} + +func bumpReleaser(releaser *devopsv1alpha1.Releaser) { + currentVersion := releaser.Spec.Version + nextVersion, _ := bumpVersion(currentVersion) + if strings.HasSuffix(releaser.Name, currentVersion) { + nameWithoutVersion := strings.ReplaceAll(releaser.Name, currentVersion, "") + releaser.Name = nameWithoutVersion + nextVersion + } + + releaser.Spec.Phase = devopsv1alpha1.PhaseDraft + releaser.Spec.Version = nextVersion + + for i, _ := range releaser.Spec.Repositories { + repo := &releaser.Spec.Repositories[i] + repo.Version, _ = bumpVersion(repo.Version) + } + return +} + +func bumpReleaserAsData(data []byte) (result []byte, filename string, err error) { + targetReleaser := &devopsv1alpha1.Releaser{} + if err = yaml.Unmarshal(data, targetReleaser); err == nil { + bumpReleaser(targetReleaser) + filename = targetReleaser.Name + ".yaml" + result, err = yaml.Marshal(targetReleaser) + } + return +} diff --git a/controllers/version_test.go b/controllers/version_test.go new file mode 100644 index 0000000..694a775 --- /dev/null +++ b/controllers/version_test.go @@ -0,0 +1,172 @@ +package controllers + +import ( + "fmt" + devopsv1alpha1 "github.com/kubesphere-sigs/ks-releaser/api/v1alpha1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "reflect" + "strings" + "testing" +) + +func TestVersionBump(t *testing.T) { + type testCase struct { + name string + arg struct { + version string + } + wantErr bool + wantVersion string + } + + testCases := []testCase{{ + name: "invalid version string", + arg: struct{ version string }{ + version: "abc", + }, + wantErr: true, + wantVersion: "abc", + }, { + name: "valid version string", + arg: struct{ version string }{ + version: "v1.0.0", + }, + wantErr: false, + wantVersion: "v1.0.1", + }, { + name: "valid version string, without patch number", + arg: struct{ version string }{ + version: "v1.0", + }, + wantErr: false, + wantVersion: "v1.0.1", + }} + + for i, _ := range testCases { + caseItem := testCases[i] + + nextVersion, err := bumpVersion(caseItem.arg.version) + if caseItem.wantErr { + assert.NotNil(t, err, fmt.Sprintf("test failed with case[%d]", i)) + } + + assert.Equal(t, caseItem.wantVersion, nextVersion, fmt.Sprintf("test failed with case[%d]", i)) + } +} + +func Test_bumpReleaser(t *testing.T) { + type args struct { + releaser *devopsv1alpha1.Releaser + } + tests := []struct { + name string + args args + wantResult *devopsv1alpha1.Releaser + wantErr bool + }{{ + name: "only have main version", + args: args{ + releaser: &devopsv1alpha1.Releaser{ + Spec: devopsv1alpha1.ReleaserSpec{Version: "v1.0.1"}, + }, + }, + wantErr: false, + wantResult: &devopsv1alpha1.Releaser{ + Spec: devopsv1alpha1.ReleaserSpec{ + Phase: devopsv1alpha1.PhaseDraft, + Version: "v1.0.2"}, + }, + }, { + name: "have repositories", + args: args{ + releaser: &devopsv1alpha1.Releaser{ + Spec: devopsv1alpha1.ReleaserSpec{ + Version: "v1.0.1", + Repositories: []devopsv1alpha1.Repository{{ + Version: "v1.2.3", + }}, + }, + }}, + wantErr: false, + wantResult: &devopsv1alpha1.Releaser{ + Spec: devopsv1alpha1.ReleaserSpec{ + Phase: devopsv1alpha1.PhaseDraft, + Version: "v1.0.2", + Repositories: []devopsv1alpha1.Repository{{ + Version: "v1.2.4", + }}, + }, + }, + }, { + name: "bump cr name", + args: args{ + releaser: &devopsv1alpha1.Releaser{ + ObjectMeta: metav1.ObjectMeta{Name: "test-v1.0.1"}, + Spec: devopsv1alpha1.ReleaserSpec{Version: "v1.0.1"}, + }, + }, + wantErr: false, + wantResult: &devopsv1alpha1.Releaser{ + ObjectMeta: metav1.ObjectMeta{Name: "test-v1.0.2"}, + Spec: devopsv1alpha1.ReleaserSpec{ + Phase: devopsv1alpha1.PhaseDraft, + Version: "v1.0.2"}, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bumpReleaser(tt.args.releaser) + if !reflect.DeepEqual(tt.args.releaser, tt.wantResult) { + t.Errorf("bumpReleaser() gotResult = %v, want %v", tt.args.releaser, tt.wantResult) + } + }) + } +} + +func Test_bumpReleaserAsData(t *testing.T) { + type args struct { + data string + } + tests := []struct { + name string + args args + wantResult string + wantErr bool + }{{ + name: "normal case", + args: args{ + data: `apiVersion: devops.kubesphere.io/v1alpha1 +kind: Releaser +metadata: + creationTimestamp: null + name: ks-releaser-v0.0.5 +spec: + version: v0.0.5`, + }, + wantResult: `apiVersion: devops.kubesphere.io/v1alpha1 +kind: Releaser +metadata: + creationTimestamp: null + name: ks-releaser-v0.0.6 +spec: + phase: draft + secret: {} + version: v0.0.6 +status: {} +`, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResult, _, err := bumpReleaserAsData([]byte(tt.args.data)) + if (err != nil) != tt.wantErr { + t.Errorf("bumpReleaserAsData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if strings.TrimSpace((string(gotResult))) != strings.TrimSpace(tt.wantResult) { + t.Errorf("bumpReleaserAsData() gotResult = %s, want %s", string(gotResult), tt.wantResult) + } + }) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 9325b11..3f7bca8 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module github.com/kubesphere-sigs/ks-releaser go 1.16 require ( + github.com/blang/semver v3.5.1+incompatible github.com/davecgh/go-spew v1.1.1 github.com/go-git/go-git/v5 v5.4.2 + github.com/go-logr/logr v0.3.0 github.com/jenkins-x/go-scm v1.10.10 github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b gopkg.in/src-d/go-git.v4 v4.13.1 k8s.io/api v0.20.2 diff --git a/go.sum b/go.sum index 87a21d2..cb29b37 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bluekeyes/go-gitdiff v0.4.0/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/main.go b/main.go index 652cdd9..511b7bb 100644 --- a/main.go +++ b/main.go @@ -81,6 +81,7 @@ func main() { if err = (&controllers.ReleaserReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + GitCacheDir: "tmp", }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Releaser") os.Exit(1)