Repository: fluxcd/notification-controller
Pull request #86 (2020-11-22 14:07:32)

Add Azure DevOps provider
Add Azure DevOps provider as the changes to the source-controller should now support Azure DevOps. After this we should have covered the major git providers out there that have a commit status api.


Patch:
diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go
index 64d86c7..302ad5a 100644
--- a/api/v1beta1/provider_types.go
+++ b/api/v1beta1/provider_types.go
@@ -24,7 +24,7 @@ import (
 // ProviderSpec defines the desired state of Provider
 type ProviderSpec struct {
 	// Type of provider
-	// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket
+	// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops
 	// +required
 	Type string `json:"type"`
 
@@ -54,14 +54,15 @@ type ProviderSpec struct {
 }
 
 const (
-	GenericProvider   string = "generic"
-	SlackProvider     string = "slack"
-	DiscordProvider   string = "discord"
-	MSTeamsProvider   string = "msteams"
-	RocketProvider    string = "rocket"
-	GitHubProvider    string = "github"
-	GitLabProvider    string = "gitlab"
-	BitbucketProvider string = "bitbucket"
+	GenericProvider     string = "generic"
+	SlackProvider       string = "slack"
+	DiscordProvider     string = "discord"
+	MSTeamsProvider     string = "msteams"
+	RocketProvider      string = "rocket"
+	GitHubProvider      string = "github"
+	GitLabProvider      string = "gitlab"
+	BitbucketProvider   string = "bitbucket"
+	AzureDevOpsProvider string = "azuredevops"
 )
 
 // ProviderStatus defines the observed state of Provider
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
index 9d5374e..181632d 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
@@ -76,6 +76,7 @@ spec:
                 - github
                 - gitlab
                 - bitbucket
+                - azuredevops
                 type: string
               username:
                 description: Bot username for this provider
diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md
index 1c76702..0c9a595 100644
--- a/docs/spec/v1beta1/provider.md
+++ b/docs/spec/v1beta1/provider.md
@@ -50,6 +50,7 @@ Git commit status providers:
 * GitHub
 * GitLab
 * Bitbucket
+* Azure DevOps
 
 Status:
 
@@ -109,7 +110,7 @@ incoming [event](event.md) in JSON format to the webhook address.
 
 ### Git commit status
 
-The GitHub, GitLab, and Bitbucket provider is a special kind of notification
+The GitHub, GitLab, Bitbucket, and Azure DevOps provider is a special kind of notification
 provider that based on the state of a Kustomization resource,
 will update the commit status for the reconciled commit id.
 
@@ -127,9 +128,10 @@ spec:
     name: api-token
 ```
 
-GitHub and GitLab use personal access tokens to authenticate with their API.
+GitHub. GitLab, and Azure DevOps use personal access tokens to authenticate with their API.
 * [GitHub personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)
 * [GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)
+* [Azure DevOps personal access token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page)
 Both provider types require a secret in the same format, with the personal access token as the value for the token key.
 ```yaml
 apiVersion: v1
diff --git a/go.mod b/go.mod
index 7a4cd4c..137d414 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/google/go-github/v32 v32.0.0
 	github.com/hashicorp/go-retryablehttp v0.6.7
 	github.com/ktrysmt/go-bitbucket v0.6.5
+	github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
 	github.com/onsi/ginkgo v1.12.1
 	github.com/onsi/gomega v1.10.1
 	github.com/stretchr/testify v1.6.1
diff --git a/go.sum b/go.sum
index 6a0432f..14ce32a 100644
--- a/go.sum
+++ b/go.sum
@@ -286,6 +286,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s=
+github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
diff --git a/internal/notifier/azure_devops.go b/internal/notifier/azure_devops.go
new file mode 100644
index 0000000..70dc051
--- /dev/null
+++ b/internal/notifier/azure_devops.go
@@ -0,0 +1,124 @@
+/*
+Copyright 2020 The Flux 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 notifier
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/fluxcd/pkg/recorder"
+	"github.com/microsoft/azure-devops-go-api/azuredevops"
+	"github.com/microsoft/azure-devops-go-api/azuredevops/git"
+)
+
+const genre string = "fluxcd"
+
+// AzureDevOps is a Azure DevOps notifier.
+type AzureDevOps struct {
+	Project    string
+	Repo       string
+	Connection *azuredevops.Connection
+}
+
+// NewAzureDevOps creates and returns a new AzureDevOps notifier.
+func NewAzureDevOps(addr string, token string) (*AzureDevOps, error) {
+	if len(token) == 0 {
+		return nil, errors.New("azure devops token cannot be empty")
+	}
+
+	host, id, err := parseGitAddress(addr)
+	if err != nil {
+		return nil, err
+	}
+
+	comp := strings.Split(id, "/")
+	if len(comp) != 4 {
+		return nil, fmt.Errorf("invalid repository id %q", id)
+	}
+	org := comp[0]
+	proj := comp[1]
+	repo := comp[3]
+
+	c := azuredevops.NewPatConnection(fmt.Sprintf("%v/%v", host, org), token)
+	return &AzureDevOps{
+		Project:    proj,
+		Repo:       repo,
+		Connection: c,
+	}, nil
+}
+
+// Post Azure DevOps commit status
+func (a AzureDevOps) Post(event recorder.Event) error {
+	// Skip progressing events
+	if event.Reason == "Progressing" {
+		return nil
+	}
+
+	revString, ok := event.Metadata["revision"]
+	if !ok {
+		return errors.New("missing revision metadata")
+	}
+	rev, err := parseRevision(revString)
+	if err != nil {
+		return err
+	}
+	state, err := toAzureDevOpsState(event.Severity)
+	if err != nil {
+		return err
+	}
+
+	ctx := context.Background()
+	client, err := git.NewClient(ctx, a.Connection)
+	if err != nil {
+		return err
+	}
+
+	g := genre
+	name, desc := formatNameAndDescription(event)
+	args := git.CreateCommitStatusArgs{
+		Project:      &a.Project,
+		RepositoryId: &a.Repo,
+		CommitId:     &rev,
+		GitCommitStatusToCreate: &git.GitStatus{
+			Description: &desc,
+			State:       &state,
+			Context: &git.GitStatusContext{
+				Genre: &g,
+				Name:  &name,
+			},
+		},
+	}
+	_, err = client.CreateCommitStatus(ctx, args)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func toAzureDevOpsState(severity string) (git.GitStatusState, error) {
+	switch severity {
+	case recorder.EventSeverityInfo:
+		return git.GitStatusStateValues.Succeeded, nil
+	case recorder.EventSeverityError:
+		return git.GitStatusStateValues.Error, nil
+	default:
+		return "", errors.New("can't convert to azure devops state")
+	}
+}
diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go
index 1903fa9..a711089 100644
--- a/internal/notifier/factory.go
+++ b/internal/notifier/factory.go
@@ -64,6 +64,8 @@ func (f Factory) Notifier(provider string) (Interface, error) {
 		n, err = NewGitLab(f.URL, f.Token)
 	case v1beta1.BitbucketProvider:
 		n, err = NewBitbucket(f.URL, f.Token)
+	case v1beta1.AzureDevOpsProvider:
+		n, err = NewAzureDevOps(f.URL, f.Token)
 	default:
 		err = fmt.Errorf("provider %s not supported", provider)
 	}


Test patch:
diff --git a/internal/notifier/azure_devops_test.go b/internal/notifier/azure_devops_test.go
new file mode 100644
index 0000000..53d0496
--- /dev/null
+++ b/internal/notifier/azure_devops_test.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2020 The Flux 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 notifier
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewAzureDevOpsBasic(t *testing.T) {
+	a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo")
+	assert.Nil(t, err)
+	assert.Equal(t, a.Project, "bar")
+	assert.Equal(t, a.Repo, "baz")
+}
+
+func TestNewAzureDevOpsInvalidUrl(t *testing.T) {
+	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "foo")
+	assert.NotNil(t, err)
+}
+
+func TestNewAzureDevOpsMissingToken(t *testing.T) {
+	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "")
+	assert.NotNil(t, err)
+}