Merge pull request #184 from fluxcd/feature/self-signed-certs
Add self signed cert to provider
This commit is contained in:
		
						commit
						490c9550c9
					
				|  | @ -56,6 +56,11 @@ type ProviderSpec struct { | |||
| 	// using "address" as data key
 | ||||
| 	// +optional
 | ||||
| 	SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` | ||||
| 
 | ||||
| 	// CertSecretRef can be given the name of a secret containing
 | ||||
| 	// a PEM-encoded CA certificate (`caFile`)
 | ||||
| 	// +optional
 | ||||
| 	CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"` | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
|  |  | |||
|  | @ -215,6 +215,11 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { | |||
| 		*out = new(meta.LocalObjectReference) | ||||
| 		**out = **in | ||||
| 	} | ||||
| 	if in.CertSecretRef != nil { | ||||
| 		in, out := &in.CertSecretRef, &out.CertSecretRef | ||||
| 		*out = new(meta.LocalObjectReference) | ||||
| 		**out = **in | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpec.
 | ||||
|  |  | |||
|  | @ -50,6 +50,16 @@ spec: | |||
|                 description: HTTP/S webhook address of this provider | ||||
|                 pattern: ^(http|https):// | ||||
|                 type: string | ||||
|               certSecretRef: | ||||
|                 description: CertSecretRef can be given the name of a secret containing | ||||
|                   a PEM-encoded CA certificate (`caFile`) | ||||
|                 properties: | ||||
|                   name: | ||||
|                     description: Name of the referent | ||||
|                     type: string | ||||
|                 required: | ||||
|                 - name | ||||
|                 type: object | ||||
|               channel: | ||||
|                 description: Alert channel for this provider | ||||
|                 type: string | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ package controllers | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
|  | @ -121,7 +122,28 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider v1beta1.Prov | |||
| 		return fmt.Errorf("no address found in 'spec.address' nor in `spec.secretRef`") | ||||
| 	} | ||||
| 
 | ||||
| 	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token) | ||||
| 	var certPool *x509.CertPool | ||||
| 	if provider.Spec.CertSecretRef != nil { | ||||
| 		var secret corev1.Secret | ||||
| 		secretName := types.NamespacedName{Namespace: provider.Namespace, Name: provider.Spec.CertSecretRef.Name} | ||||
| 
 | ||||
| 		if err := r.Get(ctx, secretName, &secret); err != nil { | ||||
| 			return fmt.Errorf("failed to read secret, error: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		caFile, ok := secret.Data["caFile"] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("no caFile found in secret %s", provider.Spec.CertSecretRef.Name) | ||||
| 		} | ||||
| 
 | ||||
| 		certPool = x509.NewCertPool() | ||||
| 		ok = certPool.AppendCertsFromPEM(caFile) | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("could not append to cert pool: invalid CA found in %s", provider.Spec.CertSecretRef.Name) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool) | ||||
| 	if _, err := factory.Notifier(provider.Spec.Type); err != nil { | ||||
| 		return fmt.Errorf("failed to initialise provider, error: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -298,6 +298,21 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference | |||
| using “address” as data key</p> | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <td> | ||||
| <code>certSecretRef</code><br> | ||||
| <em> | ||||
| <a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference"> | ||||
| github.com/fluxcd/pkg/apis/meta.LocalObjectReference | ||||
| </a> | ||||
| </em> | ||||
| </td> | ||||
| <td> | ||||
| <em>(Optional)</em> | ||||
| <p>CertSecretRef can be given the name of a secret containing | ||||
| a PEM-encoded CA certificate (<code>caFile</code>)</p> | ||||
| </td> | ||||
| </tr> | ||||
| </table> | ||||
| </td> | ||||
| </tr> | ||||
|  | @ -761,6 +776,21 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference | |||
| using “address” as data key</p> | ||||
| </td> | ||||
| </tr> | ||||
| <tr> | ||||
| <td> | ||||
| <code>certSecretRef</code><br> | ||||
| <em> | ||||
| <a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference"> | ||||
| github.com/fluxcd/pkg/apis/meta.LocalObjectReference | ||||
| </a> | ||||
| </em> | ||||
| </td> | ||||
| <td> | ||||
| <em>(Optional)</em> | ||||
| <p>CertSecretRef can be given the name of a secret containing | ||||
| a PEM-encoded CA certificate (<code>caFile</code>)</p> | ||||
| </td> | ||||
| </tr> | ||||
| </tbody> | ||||
| </table> | ||||
| </div> | ||||
|  |  | |||
|  | @ -209,3 +209,16 @@ The body of the request looks like this: | |||
| ``` | ||||
| 
 | ||||
| The `involvedObject` key contains the object that triggered the event. | ||||
| 
 | ||||
| ### Self signed certificates | ||||
| 
 | ||||
| The `certSecretRef` field names a secret with TLS certificate data. This is for the purpose | ||||
| of enabling a provider to communicate with a server using a self signed cert. | ||||
| 
 | ||||
| To use the field create a secret, containing a CA file, in the same namespace and reference | ||||
| it from the provider. | ||||
| ```shell | ||||
| SECRET_NAME=tls-certs | ||||
| kubectl create secret generic $SECRET_NAME \ | ||||
|   --from-file=caFile=ca.crt | ||||
| ``` | ||||
|  |  | |||
|  | @ -18,12 +18,15 @@ package notifier | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 
 | ||||
| 	"github.com/microsoft/azure-devops-go-api/azuredevops" | ||||
| 	"github.com/microsoft/azure-devops-go-api/azuredevops/git" | ||||
| ) | ||||
|  | @ -38,7 +41,7 @@ type AzureDevOps struct { | |||
| } | ||||
| 
 | ||||
| // NewAzureDevOps creates and returns a new AzureDevOps notifier.
 | ||||
| func NewAzureDevOps(addr string, token string) (*AzureDevOps, error) { | ||||
| func NewAzureDevOps(addr string, token string, certPool *x509.CertPool) (*AzureDevOps, error) { | ||||
| 	if len(token) == 0 { | ||||
| 		return nil, errors.New("azure devops token cannot be empty") | ||||
| 	} | ||||
|  | @ -58,6 +61,11 @@ func NewAzureDevOps(addr string, token string) (*AzureDevOps, error) { | |||
| 
 | ||||
| 	orgURL := fmt.Sprintf("%v/%v", host, org) | ||||
| 	connection := azuredevops.NewPatConnection(orgURL, token) | ||||
| 	if certPool != nil { | ||||
| 		connection.TlsConfig = &tls.Config{ | ||||
| 			RootCAs: certPool, | ||||
| 		} | ||||
| 	} | ||||
| 	client := connection.GetClientByUrl(orgURL) | ||||
| 	gitClient := &git.ClientImpl{ | ||||
| 		Client: *client, | ||||
|  |  | |||
|  | @ -24,19 +24,19 @@ import ( | |||
| ) | ||||
| 
 | ||||
| func TestNewAzureDevOpsBasic(t *testing.T) { | ||||
| 	a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo") | ||||
| 	a, err := NewAzureDevOps("https://dev.azure.com/foo/bar/_git/baz", "foo", nil) | ||||
| 	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") | ||||
| 	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "foo", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestNewAzureDevOpsMissingToken(t *testing.T) { | ||||
| 	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "") | ||||
| 	_, err := NewAzureDevOps("https://dev.azure.com/foo/bar/baz", "", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,8 +17,11 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
|  | @ -33,7 +36,7 @@ type Bitbucket struct { | |||
| } | ||||
| 
 | ||||
| // NewBitbucket creates and returns a new Bitbucket notifier.
 | ||||
| func NewBitbucket(addr string, token string) (*Bitbucket, error) { | ||||
| func NewBitbucket(addr string, token string, certPool *x509.CertPool) (*Bitbucket, error) { | ||||
| 	if len(token) == 0 { | ||||
| 		return nil, errors.New("bitbucket token cannot be empty") | ||||
| 	} | ||||
|  | @ -57,10 +60,21 @@ func NewBitbucket(addr string, token string) (*Bitbucket, error) { | |||
| 	owner := comp[0] | ||||
| 	repo := comp[1] | ||||
| 
 | ||||
| 	client := bitbucket.NewBasicAuth(username, password) | ||||
| 	if certPool != nil { | ||||
| 		tr := &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				RootCAs: certPool, | ||||
| 			}, | ||||
| 		} | ||||
| 		hc := &http.Client{Transport: tr} | ||||
| 		client.HttpClient = hc | ||||
| 	} | ||||
| 
 | ||||
| 	return &Bitbucket{ | ||||
| 		Owner:  owner, | ||||
| 		Repo:   repo, | ||||
| 		Client: bitbucket.NewBasicAuth(username, password), | ||||
| 		Client: client, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,18 +23,18 @@ import ( | |||
| ) | ||||
| 
 | ||||
| func TestNewBitbucketBasic(t *testing.T) { | ||||
| 	b, err := NewBitbucket("https://bitbucket.org/foo/bar", "foo:bar") | ||||
| 	b, err := NewBitbucket("https://bitbucket.org/foo/bar", "foo:bar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, b.Owner, "foo") | ||||
| 	assert.Equal(t, b.Repo, "bar") | ||||
| } | ||||
| 
 | ||||
| func TestNewBitbucketInvalidUrl(t *testing.T) { | ||||
| 	_, err := NewBitbucket("https://bitbucket.org/foo/bar/baz", "foo:bar") | ||||
| 	_, err := NewBitbucket("https://bitbucket.org/foo/bar/baz", "foo:bar", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestNewBitbucketInvalidToken(t *testing.T) { | ||||
| 	_, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar") | ||||
| 	_, err := NewBitbucket("https://bitbucket.org/foo/bar", "bar", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
|  |  | |||
|  | @ -17,6 +17,8 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
|  | @ -30,16 +32,30 @@ import ( | |||
| 
 | ||||
| type requestOptFunc func(*retryablehttp.Request) | ||||
| 
 | ||||
| func postMessage(address, proxy string, payload interface{}, reqOpts ...requestOptFunc) error { | ||||
| func postMessage(address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error { | ||||
| 	httpClient := retryablehttp.NewClient() | ||||
| 	if certPool != nil { | ||||
| 		httpClient.HTTPClient.Transport = &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				RootCAs: certPool, | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if proxy != "" { | ||||
| 		proxyURL, err := url.Parse(proxy) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("unable to parse proxy URL '%s', error: %w", proxy, err) | ||||
| 		} | ||||
| 		var tlsConfig *tls.Config | ||||
| 		if certPool != nil { | ||||
| 			tlsConfig = &tls.Config{ | ||||
| 				RootCAs: certPool, | ||||
| 			} | ||||
| 		} | ||||
| 		httpClient.HTTPClient.Transport = &http.Transport{ | ||||
| 			Proxy: http.ProxyURL(proxyURL), | ||||
| 			Proxy:           http.ProxyURL(proxyURL), | ||||
| 			TLSClientConfig: tlsConfig, | ||||
| 			DialContext: (&net.Dialer{ | ||||
| 				Timeout:   15 * time.Second, | ||||
| 				KeepAlive: 30 * time.Second, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
|  | @ -42,7 +43,28 @@ func Test_postMessage(t *testing.T) { | |||
| 		require.Equal(t, "success", payload["status"]) | ||||
| 	})) | ||||
| 	defer ts.Close() | ||||
| 	err := postMessage(ts.URL, "", map[string]string{"status": "success"}) | ||||
| 	err := postMessage(ts.URL, "", nil, map[string]string{"status": "success"}) | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
| 
 | ||||
| func Test_postSelfSignedCert(t *testing.T) { | ||||
| 	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		b, err := ioutil.ReadAll(r.Body) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		var payload = make(map[string]string) | ||||
| 		err = json.Unmarshal(b, &payload) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		require.Equal(t, "success", payload["status"]) | ||||
| 	})) | ||||
| 	defer ts.Close() | ||||
| 
 | ||||
| 	cert, err := x509.ParseCertificate(ts.TLS.Certificates[0].Certificate[0]) | ||||
| 	require.NoError(t, err) | ||||
| 	certpool := x509.NewCertPool() | ||||
| 	certpool.AddCert(cert) | ||||
| 	err = postMessage(ts.URL, "", certpool, map[string]string{"status": "success"}) | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ func (s *Discord) Post(event events.Event) error { | |||
| 
 | ||||
| 	payload.Attachments = []SlackAttachment{a} | ||||
| 
 | ||||
| 	err := postMessage(s.URL, s.ProxyURL, payload) | ||||
| 	err := postMessage(s.URL, s.ProxyURL, nil, payload) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/fluxcd/notification-controller/api/v1beta1" | ||||
|  | @ -28,15 +29,17 @@ type Factory struct { | |||
| 	Username string | ||||
| 	Channel  string | ||||
| 	Token    string | ||||
| 	CertPool *x509.CertPool | ||||
| } | ||||
| 
 | ||||
| func NewFactory(url string, proxy string, username string, channel string, token string) *Factory { | ||||
| func NewFactory(url string, proxy string, username string, channel string, token string, certPool *x509.CertPool) *Factory { | ||||
| 	return &Factory{ | ||||
| 		URL:      url, | ||||
| 		ProxyURL: proxy, | ||||
| 		Channel:  channel, | ||||
| 		Username: username, | ||||
| 		Token:    token, | ||||
| 		CertPool: certPool, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -49,29 +52,29 @@ func (f Factory) Notifier(provider string) (Interface, error) { | |||
| 	var err error | ||||
| 	switch provider { | ||||
| 	case v1beta1.GenericProvider: | ||||
| 		n, err = NewForwarder(f.URL, f.ProxyURL) | ||||
| 		n, err = NewForwarder(f.URL, f.ProxyURL, f.CertPool) | ||||
| 	case v1beta1.SlackProvider: | ||||
| 		n, err = NewSlack(f.URL, f.ProxyURL, f.Username, f.Channel) | ||||
| 	case v1beta1.DiscordProvider: | ||||
| 		n, err = NewDiscord(f.URL, f.ProxyURL, f.Username, f.Channel) | ||||
| 	case v1beta1.RocketProvider: | ||||
| 		n, err = NewRocket(f.URL, f.ProxyURL, f.Username, f.Channel) | ||||
| 		n, err = NewRocket(f.URL, f.ProxyURL, f.CertPool, f.Username, f.Channel) | ||||
| 	case v1beta1.MSTeamsProvider: | ||||
| 		n, err = NewMSTeams(f.URL, f.ProxyURL) | ||||
| 	case v1beta1.GitHubProvider: | ||||
| 		n, err = NewGitHub(f.URL, f.Token) | ||||
| 		n, err = NewGitHub(f.URL, f.Token, f.CertPool) | ||||
| 	case v1beta1.GitLabProvider: | ||||
| 		n, err = NewGitLab(f.URL, f.Token) | ||||
| 		n, err = NewGitLab(f.URL, f.Token, f.CertPool) | ||||
| 	case v1beta1.BitbucketProvider: | ||||
| 		n, err = NewBitbucket(f.URL, f.Token) | ||||
| 		n, err = NewBitbucket(f.URL, f.Token, f.CertPool) | ||||
| 	case v1beta1.AzureDevOpsProvider: | ||||
| 		n, err = NewAzureDevOps(f.URL, f.Token) | ||||
| 		n, err = NewAzureDevOps(f.URL, f.Token, f.CertPool) | ||||
| 	case v1beta1.GoogleChatProvider: | ||||
| 		n, err = NewGoogleChat(f.URL, f.ProxyURL) | ||||
| 	case v1beta1.WebexProvider: | ||||
| 		n, err = NewWebex(f.URL, f.ProxyURL) | ||||
| 		n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool) | ||||
| 	case v1beta1.SentryProvider: | ||||
| 		n, err = NewSentry(f.URL) | ||||
| 		n, err = NewSentry(f.CertPool, f.URL) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("provider %s not supported", provider) | ||||
| 	} | ||||
|  |  | |||
|  | @ -17,10 +17,12 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 
 | ||||
| 	"github.com/hashicorp/go-retryablehttp" | ||||
| ) | ||||
| 
 | ||||
|  | @ -33,9 +35,10 @@ const NotificationHeader = "gotk-component" | |||
| type Forwarder struct { | ||||
| 	URL      string | ||||
| 	ProxyURL string | ||||
| 	CertPool *x509.CertPool | ||||
| } | ||||
| 
 | ||||
| func NewForwarder(hookURL string, proxyURL string) (*Forwarder, error) { | ||||
| func NewForwarder(hookURL string, proxyURL string, certPool *x509.CertPool) (*Forwarder, error) { | ||||
| 	if _, err := url.ParseRequestURI(hookURL); err != nil { | ||||
| 		return nil, fmt.Errorf("invalid hook URL %s: %w", hookURL, err) | ||||
| 	} | ||||
|  | @ -47,7 +50,7 @@ func NewForwarder(hookURL string, proxyURL string) (*Forwarder, error) { | |||
| } | ||||
| 
 | ||||
| func (f *Forwarder) Post(event events.Event) error { | ||||
| 	err := postMessage(f.URL, f.ProxyURL, event, func(req *retryablehttp.Request) { | ||||
| 	err := postMessage(f.URL, f.ProxyURL, f.CertPool, event, func(req *retryablehttp.Request) { | ||||
| 		req.Header.Set(NotificationHeader, event.ReportingController) | ||||
| 	}) | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,12 +18,13 @@ package notifier | |||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
|  | @ -41,7 +42,7 @@ func TestForwarder_Post(t *testing.T) { | |||
| 	})) | ||||
| 	defer ts.Close() | ||||
| 
 | ||||
| 	forwarder, err := NewForwarder(ts.URL, "") | ||||
| 	forwarder, err := NewForwarder(ts.URL, "", nil) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	err = forwarder.Post(testEvent()) | ||||
|  |  | |||
|  | @ -18,13 +18,17 @@ package notifier | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 
 | ||||
| 	"github.com/google/go-github/v32/github" | ||||
| 	"golang.org/x/oauth2" | ||||
| ) | ||||
|  | @ -35,7 +39,7 @@ type GitHub struct { | |||
| 	Client *github.Client | ||||
| } | ||||
| 
 | ||||
| func NewGitHub(addr string, token string) (*GitHub, error) { | ||||
| func NewGitHub(addr string, token string, certPool *x509.CertPool) (*GitHub, error) { | ||||
| 	if len(token) == 0 { | ||||
| 		return nil, errors.New("github token cannot be empty") | ||||
| 	} | ||||
|  | @ -59,6 +63,17 @@ func NewGitHub(addr string, token string) (*GitHub, error) { | |||
| 	tc := oauth2.NewClient(context.Background(), ts) | ||||
| 	client := github.NewClient(tc) | ||||
| 	if baseUrl.Host != "github.com" { | ||||
| 		if certPool != nil { | ||||
| 			tr := &http.Transport{ | ||||
| 				TLSClientConfig: &tls.Config{ | ||||
| 					RootCAs: certPool, | ||||
| 				}, | ||||
| 			} | ||||
| 			hc := &http.Client{Transport: tr} | ||||
| 			ctx := context.WithValue(context.Background(), oauth2.HTTPClient, hc) | ||||
| 			tc = oauth2.NewClient(ctx, ts) | ||||
| 		} | ||||
| 
 | ||||
| 		client, err = github.NewEnterpriseClient(host, host, tc) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("could not create enterprise GitHub client: %v", err) | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ import ( | |||
| ) | ||||
| 
 | ||||
| func TestNewGitHubBasic(t *testing.T) { | ||||
| 	g, err := NewGitHub("https://github.com/foo/bar", "foobar") | ||||
| 	g, err := NewGitHub("https://github.com/foo/bar", "foobar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, g.Owner, "foo") | ||||
| 	assert.Equal(t, g.Repo, "bar") | ||||
|  | @ -32,7 +32,7 @@ func TestNewGitHubBasic(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestNewEmterpriseGitHubBasic(t *testing.T) { | ||||
| 	g, err := NewGitHub("https://foobar.com/foo/bar", "foobar") | ||||
| 	g, err := NewGitHub("https://foobar.com/foo/bar", "foobar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, g.Owner, "foo") | ||||
| 	assert.Equal(t, g.Repo, "bar") | ||||
|  | @ -40,12 +40,12 @@ func TestNewEmterpriseGitHubBasic(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestNewGitHubInvalidUrl(t *testing.T) { | ||||
| 	_, err := NewGitHub("https://github.com/foo/bar/baz", "foobar") | ||||
| 	_, err := NewGitHub("https://github.com/foo/bar/baz", "foobar", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
| 
 | ||||
| func TestNewGitHubEmptyToken(t *testing.T) { | ||||
| 	_, err := NewGitHub("https://github.com/foo/bar", "") | ||||
| 	_, err := NewGitHub("https://github.com/foo/bar", "", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,7 +17,10 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"github.com/xanzy/go-gitlab" | ||||
|  | @ -28,7 +31,7 @@ type GitLab struct { | |||
| 	Client *gitlab.Client | ||||
| } | ||||
| 
 | ||||
| func NewGitLab(addr string, token string) (*GitLab, error) { | ||||
| func NewGitLab(addr string, token string, certPool *x509.CertPool) (*GitLab, error) { | ||||
| 	if len(token) == 0 { | ||||
| 		return nil, errors.New("gitlab token cannot be empty") | ||||
| 	} | ||||
|  | @ -38,8 +41,17 @@ func NewGitLab(addr string, token string) (*GitLab, error) { | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	opt := gitlab.WithBaseURL(host) | ||||
| 	client, err := gitlab.NewClient(token, opt) | ||||
| 	opts := []gitlab.ClientOptionFunc{gitlab.WithBaseURL(host)} | ||||
| 	if certPool != nil { | ||||
| 		tr := &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				RootCAs: certPool, | ||||
| 			}, | ||||
| 		} | ||||
| 		hc := &http.Client{Transport: tr} | ||||
| 		opts = append(opts, gitlab.WithHTTPClient(hc)) | ||||
| 	} | ||||
| 	client, err := gitlab.NewClient(token, opts...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  |  | |||
|  | @ -23,25 +23,25 @@ import ( | |||
| ) | ||||
| 
 | ||||
| func TestNewGitLabBasic(t *testing.T) { | ||||
| 	g, err := NewGitLab("https://gitlab.com/foo/bar", "foobar") | ||||
| 	g, err := NewGitLab("https://gitlab.com/foo/bar", "foobar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, g.Id, "foo/bar") | ||||
| } | ||||
| 
 | ||||
| func TestNewGitLabSubgroups(t *testing.T) { | ||||
| 	g, err := NewGitLab("https://gitlab.com/foo/bar/baz", "foobar") | ||||
| 	g, err := NewGitLab("https://gitlab.com/foo/bar/baz", "foobar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, g.Id, "foo/bar/baz") | ||||
| } | ||||
| 
 | ||||
| func TestNewGitLabSelfHosted(t *testing.T) { | ||||
| 	g, err := NewGitLab("https://example.com/foo/bar", "foo:bar") | ||||
| 	g, err := NewGitLab("https://example.com/foo/bar", "foo:bar", nil) | ||||
| 	assert.Nil(t, err) | ||||
| 	assert.Equal(t, g.Id, "foo/bar") | ||||
| 	assert.Equal(t, g.Client.BaseURL().Host, "example.com") | ||||
| } | ||||
| 
 | ||||
| func TestNewGitLabEmptyToken(t *testing.T) { | ||||
| 	_, err := NewGitLab("https://gitlab.com/foo/bar", "") | ||||
| 	_, err := NewGitLab("https://gitlab.com/foo/bar", "", nil) | ||||
| 	assert.NotNil(t, err) | ||||
| } | ||||
|  |  | |||
|  | @ -141,7 +141,7 @@ func (s *GoogleChat) Post(event events.Event) error { | |||
| 		Cards: []GoogleChatCard{card}, | ||||
| 	} | ||||
| 
 | ||||
| 	err := postMessage(s.URL, s.ProxyURL, payload) | ||||
| 	err := postMessage(s.URL, s.ProxyURL, nil, payload) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
|  | @ -31,10 +32,11 @@ type Rocket struct { | |||
| 	ProxyURL string | ||||
| 	Username string | ||||
| 	Channel  string | ||||
| 	CertPool *x509.CertPool | ||||
| } | ||||
| 
 | ||||
| // NewRocket validates the Rocket URL and returns a Rocket object
 | ||||
| func NewRocket(hookURL string, proxyURL string, username string, channel string) (*Rocket, error) { | ||||
| func NewRocket(hookURL string, proxyURL string, certPool *x509.CertPool, username string, channel string) (*Rocket, error) { | ||||
| 	_, err := url.ParseRequestURI(hookURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("invalid Rocket hook URL %s", hookURL) | ||||
|  | @ -53,6 +55,7 @@ func NewRocket(hookURL string, proxyURL string, username string, channel string) | |||
| 		URL:      hookURL, | ||||
| 		ProxyURL: proxyURL, | ||||
| 		Username: username, | ||||
| 		CertPool: certPool, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  | @ -88,7 +91,7 @@ func (s *Rocket) Post(event events.Event) error { | |||
| 
 | ||||
| 	payload.Attachments = []SlackAttachment{a} | ||||
| 
 | ||||
| 	err := postMessage(s.URL, s.ProxyURL, payload) | ||||
| 	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ func TestRocket_Post(t *testing.T) { | |||
| 	})) | ||||
| 	defer ts.Close() | ||||
| 
 | ||||
| 	rocket, err := NewRocket(ts.URL, "", "test", "test") | ||||
| 	rocket, err := NewRocket(ts.URL, "", nil, "test", "test") | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	err = rocket.Post(testEvent()) | ||||
|  |  | |||
|  | @ -17,7 +17,11 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"github.com/getsentry/sentry-go" | ||||
| ) | ||||
|  | @ -28,9 +32,18 @@ type Sentry struct { | |||
| } | ||||
| 
 | ||||
| // NewSentry creates a Sentry client from the provided Data Source Name (DSN)
 | ||||
| func NewSentry(dsn string) (*Sentry, error) { | ||||
| func NewSentry(certPool *x509.CertPool, dsn string) (*Sentry, error) { | ||||
| 	var tr *http.Transport | ||||
| 	if certPool != nil { | ||||
| 		tr = &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				RootCAs: certPool, | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 	client, err := sentry.NewClient(sentry.ClientOptions{ | ||||
| 		Dsn: dsn, | ||||
| 		Dsn:           dsn, | ||||
| 		HTTPTransport: tr, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
|  |  | |||
|  | @ -17,19 +17,20 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/runtime/events" | ||||
| 	"github.com/getsentry/sentry-go" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func TestNewSentry(t *testing.T) { | ||||
| 	s, err := NewSentry("https://test@localhost/1") | ||||
| 	s, err := NewSentry(nil, "https://test@localhost/1") | ||||
| 	require.NoError(t, err) | ||||
| 	assert.Equal(t, s.Client.Options().Dsn, "https://test@localhost/1") | ||||
| } | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ func (s *Slack) Post(event events.Event) error { | |||
| 
 | ||||
| 	payload.Attachments = []SlackAttachment{a} | ||||
| 
 | ||||
| 	err := postMessage(s.URL, s.ProxyURL, payload) | ||||
| 	err := postMessage(s.URL, s.ProxyURL, nil, payload) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -98,7 +98,7 @@ func (s *MSTeams) Post(event events.Event) error { | |||
| 		payload.ThemeColor = "FF0000" | ||||
| 	} | ||||
| 
 | ||||
| 	err := postMessage(s.URL, s.ProxyURL, payload) | ||||
| 	err := postMessage(s.URL, s.ProxyURL, nil, payload) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ limitations under the License. | |||
| package notifier | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | @ -28,6 +29,7 @@ import ( | |||
| type Webex struct { | ||||
| 	URL      string | ||||
| 	ProxyURL string | ||||
| 	CertPool *x509.CertPool | ||||
| } | ||||
| 
 | ||||
| // WebexPayload holds the message text
 | ||||
|  | @ -37,7 +39,7 @@ type WebexPayload struct { | |||
| } | ||||
| 
 | ||||
| // NewWebex validates the Webex URL and returns a Webex object
 | ||||
| func NewWebex(hookURL, proxyURL string) (*Webex, error) { | ||||
| func NewWebex(hookURL, proxyURL string, certPool *x509.CertPool) (*Webex, error) { | ||||
| 	_, err := url.ParseRequestURI(hookURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("invalid Webex hook URL %s", hookURL) | ||||
|  | @ -71,7 +73,7 @@ func (s *Webex) Post(event events.Event) error { | |||
| 		Markdown: markdown, | ||||
| 	} | ||||
| 
 | ||||
| 	if err := postMessage(s.URL, s.ProxyURL, payload); err != nil { | ||||
| 	if err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload); err != nil { | ||||
| 		return fmt.Errorf("postMessage failed: %w", err) | ||||
| 	} | ||||
| 	return nil | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ func TestWebex_Post(t *testing.T) { | |||
| 	})) | ||||
| 	defer ts.Close() | ||||
| 
 | ||||
| 	webex, err := NewWebex(ts.URL, "") | ||||
| 	webex, err := NewWebex(ts.URL, "", nil) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	err = webex.Post(testEvent()) | ||||
|  | @ -47,7 +47,7 @@ func TestWebex_Post(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestWebex_PostUpdate(t *testing.T) { | ||||
| 	webex, err := NewWebex("http://localhost", "") | ||||
| 	webex, err := NewWebex("http://localhost", "", nil) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	event := testEvent() | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ package server | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
|  | @ -156,6 +157,40 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) | |||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			var certPool *x509.CertPool | ||||
| 			if provider.Spec.CertSecretRef != nil { | ||||
| 				var secret corev1.Secret | ||||
| 				secretName := types.NamespacedName{Namespace: alert.Namespace, Name: provider.Spec.CertSecretRef.Name} | ||||
| 
 | ||||
| 				err = s.kubeClient.Get(ctx, secretName, &secret) | ||||
| 				if err != nil { | ||||
| 					s.logger.Error(err, "failed to read secret", | ||||
| 						"reconciler kind", v1beta1.ProviderKind, | ||||
| 						"name", providerName.Name, | ||||
| 						"namespace", providerName.Namespace) | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				caFile, ok := secret.Data["caFile"] | ||||
| 				if !ok { | ||||
| 					s.logger.Error(err, "failed to read secret key caFile", | ||||
| 						"reconciler kind", v1beta1.ProviderKind, | ||||
| 						"name", providerName.Name, | ||||
| 						"namespace", providerName.Namespace) | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				certPool = x509.NewCertPool() | ||||
| 				ok = certPool.AppendCertsFromPEM(caFile) | ||||
| 				if !ok { | ||||
| 					s.logger.Error(err, "could not append to cert pool", | ||||
| 						"reconciler kind", v1beta1.ProviderKind, | ||||
| 						"name", providerName.Name, | ||||
| 						"namespace", providerName.Namespace) | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if webhook == "" { | ||||
| 				s.logger.Error(nil, "provider has no address", | ||||
| 					"reconciler kind", v1beta1.ProviderKind, | ||||
|  | @ -164,7 +199,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) | |||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token) | ||||
| 			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool) | ||||
| 			sender, err := factory.Notifier(provider.Spec.Type) | ||||
| 			if err != nil { | ||||
| 				s.logger.Error(err, "failed to initialise provider", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue