Skip to content

Commit 4d1c503

Browse files
authored
Merge pull request #1154 from cappyzawa/feat/google-pubsub-workload-identity
[RFC-0010] Add object-level workload identity support to Google Pub/Sub notifier
2 parents eddaf14 + 039cd81 commit 4d1c503

File tree

8 files changed

+316
-141
lines changed

8 files changed

+316
-141
lines changed

api/v1beta3/provider_types.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,22 @@ type ProviderSpec struct {
116116
// +optional
117117
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
118118

119-
// ServiceAccountName is the name of the service account used to
120-
// authenticate with services from cloud providers. An error is thrown if a
121-
// static credential is also defined inside the Secret referenced by the
122-
// SecretRef.
119+
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to
120+
// authenticate with cloud provider services through workload identity.
121+
// This enables multi-tenant authentication without storing static credentials.
122+
//
123+
// Supported provider types: azureeventhub, azuredevops, googlepubsub
124+
//
125+
// When specified, the controller will:
126+
// 1. Create an OIDC token for the specified ServiceAccount
127+
// 2. Exchange it for cloud provider credentials via STS
128+
// 3. Use the obtained credentials for API authentication
129+
//
130+
// When unspecified, controller-level authentication is used (single-tenant).
131+
//
132+
// An error is thrown if static credentials are also defined in SecretRef.
133+
// This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.
134+
//
123135
// +optional
124136
ServiceAccountName string `json:"serviceAccountName,omitempty"`
125137

config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -511,10 +511,21 @@ spec:
511511
type: object
512512
serviceAccountName:
513513
description: |-
514-
ServiceAccountName is the name of the service account used to
515-
authenticate with services from cloud providers. An error is thrown if a
516-
static credential is also defined inside the Secret referenced by the
517-
SecretRef.
514+
ServiceAccountName is the name of the Kubernetes ServiceAccount used to
515+
authenticate with cloud provider services through workload identity.
516+
This enables multi-tenant authentication without storing static credentials.
517+
518+
Supported provider types: azureeventhub, azuredevops, googlepubsub
519+
520+
When specified, the controller will:
521+
1. Create an OIDC token for the specified ServiceAccount
522+
2. Exchange it for cloud provider credentials via STS
523+
3. Use the obtained credentials for API authentication
524+
525+
When unspecified, controller-level authentication is used (single-tenant).
526+
527+
An error is thrown if static credentials are also defined in SecretRef.
528+
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.
518529
type: string
519530
suspend:
520531
description: |-

docs/api/v1beta3/notification.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,17 @@ string
375375
</td>
376376
<td>
377377
<em>(Optional)</em>
378-
<p>ServiceAccountName is the name of the service account used to
379-
authenticate with services from cloud providers. An error is thrown if a
380-
static credential is also defined inside the Secret referenced by the
381-
SecretRef.</p>
378+
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
379+
authenticate with cloud provider services through workload identity.
380+
This enables multi-tenant authentication without storing static credentials.</p>
381+
<p>Supported provider types: azureeventhub, azuredevops, googlepubsub</p>
382+
<p>When specified, the controller will:
383+
1. Create an OIDC token for the specified ServiceAccount
384+
2. Exchange it for cloud provider credentials via STS
385+
3. Use the obtained credentials for API authentication</p>
386+
<p>When unspecified, controller-level authentication is used (single-tenant).</p>
387+
<p>An error is thrown if static credentials are also defined in SecretRef.
388+
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.</p>
382389
</td>
383390
</tr>
384391
<tr>
@@ -716,10 +723,17 @@ string
716723
</td>
717724
<td>
718725
<em>(Optional)</em>
719-
<p>ServiceAccountName is the name of the service account used to
720-
authenticate with services from cloud providers. An error is thrown if a
721-
static credential is also defined inside the Secret referenced by the
722-
SecretRef.</p>
726+
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
727+
authenticate with cloud provider services through workload identity.
728+
This enables multi-tenant authentication without storing static credentials.</p>
729+
<p>Supported provider types: azureeventhub, azuredevops, googlepubsub</p>
730+
<p>When specified, the controller will:
731+
1. Create an OIDC token for the specified ServiceAccount
732+
2. Exchange it for cloud provider credentials via STS
733+
3. Use the obtained credentials for API authentication</p>
734+
<p>When unspecified, controller-level authentication is used (single-tenant).</p>
735+
<p>An error is thrown if static credentials are also defined in SecretRef.
736+
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.</p>
723737
</td>
724738
</tr>
725739
<tr>

docs/spec/v1beta3/providers.md

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,75 @@ stringData:
778778
attr2-name: attr2-value
779779
```
780780

781+
###### Google Pub/Sub with Workload Identity Example
782+
783+
To configure a Provider for Google Pub/Sub authenticating with Workload Identity,
784+
create a Kubernetes ServiceAccount with the appropriate annotations, and a
785+
`googlepubsub` Provider with the ServiceAccount reference. This method eliminates
786+
the need for JSON credentials and enables multi-tenant authentication.
787+
788+
**Single tenant approach**
789+
790+
This approach uses the notification-controller service account for setting up
791+
authentication.
792+
793+
- In the default installation, the notification-controller service account is
794+
located in the `flux-system` namespace with name `notification-controller`.
795+
796+
- Configure workload identity with notification-controller as described in the
797+
docs [here](/flux/integrations/gcp/#for-google-cloud-pubsub).
798+
799+
```yaml
800+
---
801+
apiVersion: notification.toolkit.fluxcd.io/v1beta3
802+
kind: Provider
803+
metadata:
804+
name: googlepubsub-controller-level
805+
namespace: flux-system
806+
spec:
807+
type: googlepubsub
808+
address: <GCP Project ID>
809+
channel: <Pub/Sub Topic ID>
810+
# No serviceAccountName specified - uses controller's identity
811+
```
812+
813+
**Multi-tenant approach**
814+
815+
For multi-tenant clusters, set `.spec.serviceAccountName` of the provider to
816+
the service account to be used for authentication. Ensure that the service
817+
account has the appropriate annotations for GCP workload identity.
818+
819+
```yaml
820+
---
821+
apiVersion: notification.toolkit.fluxcd.io/v1beta3
822+
kind: Provider
823+
metadata:
824+
name: googlepubsub-tenant-a
825+
namespace: tenant-a
826+
spec:
827+
type: googlepubsub
828+
address: <GCP Project ID>
829+
channel: <Pub/Sub Topic ID>
830+
serviceAccountName: tenant-a-pubsub-sa
831+
---
832+
apiVersion: v1
833+
kind: ServiceAccount
834+
metadata:
835+
name: tenant-a-pubsub-sa
836+
namespace: tenant-a
837+
annotations:
838+
# For GKE Workload Identity
839+
iam.gke.io/gcp-service-account: tenant-a-pubsub@<GCP_PROJECT_ID>.iam.gserviceaccount.com
840+
# For Workload Identity Federation (non-GKE)
841+
gcp.auth.fluxcd.io/workload-identity-provider: projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/<PROVIDER_ID>
842+
```
843+
844+
**Note:** Object-level authentication requires the `ObjectLevelWorkloadIdentity` feature gate to be enabled:
845+
846+
```bash
847+
--feature-gates=ObjectLevelWorkloadIdentity=true
848+
```
849+
781850
##### Opsgenie
782851

783852
When `.spec.type` is set to `opsgenie`, the controller will send a payload for
@@ -1837,9 +1906,71 @@ Managed Identity authentication can be setup using Azure Workload identity.
18371906
uses the correct namespace and name of the service account. For more details,
18381907
please refer to this
18391908
[guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html#6-establish-federated-identity-credential-between-the-identity-and-the-service-account-issuer--subject).
1840-
The service account used for authentication can be single-tenant
1841-
(controller-level) or multi-tenant(object-level). For a complete guide on how to
1842-
set up authentication, see the integration [docs](/flux/integrations/azure/).
1909+
The service account used for authentication can be single-tenant or
1910+
multi-tenant. For a complete guide on how to set up authentication, see the
1911+
integration [docs](/flux/integrations/azure/).
1912+
1913+
##### Azure DevOps with Workload Identity Example
1914+
1915+
**Single tenant approach**
1916+
1917+
This approach uses the notification-controller service account for setting up
1918+
authentication.
1919+
1920+
- In the default installation, the notification-controller service account is
1921+
located in the `flux-system` namespace with name `notification-controller`.
1922+
1923+
- Configure workload identity with notification-controller as described in the
1924+
docs [here](/flux/installation/configuration/workload-identity/).
1925+
1926+
```yaml
1927+
---
1928+
apiVersion: notification.toolkit.fluxcd.io/v1beta3
1929+
kind: Provider
1930+
metadata:
1931+
name: azuredevops-controller-level
1932+
namespace: flux-system
1933+
spec:
1934+
type: azuredevops
1935+
address: https://dev.azure.com/<organization>/<project>/_git/<repository>
1936+
# No serviceAccountName specified - uses controller's identity
1937+
```
1938+
1939+
**Multi-tenant approach**
1940+
1941+
For multi-tenant clusters, set `.spec.serviceAccountName` of the provider to
1942+
the service account to be used for authentication. Ensure that the service
1943+
account has the
1944+
[annotations](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet#service-account-annotations)
1945+
for the client-id and tenant-id of the managed identity.
1946+
1947+
```yaml
1948+
---
1949+
apiVersion: notification.toolkit.fluxcd.io/v1beta3
1950+
kind: Provider
1951+
metadata:
1952+
name: azuredevops-tenant-a
1953+
namespace: tenant-a
1954+
spec:
1955+
type: azuredevops
1956+
address: https://dev.azure.com/<organization>/<project>/_git/<repository>
1957+
serviceAccountName: tenant-a-devops-sa
1958+
---
1959+
apiVersion: v1
1960+
kind: ServiceAccount
1961+
metadata:
1962+
name: tenant-a-devops-sa
1963+
namespace: tenant-a
1964+
annotations:
1965+
azure.workload.identity/client-id: <client-id-of-managed-identity>
1966+
azure.workload.identity/tenant-id: <tenant-id>
1967+
```
1968+
1969+
**Note:** Object-level authentication requires the `ObjectLevelWorkloadIdentity` feature gate to be enabled:
1970+
1971+
```bash
1972+
--feature-gates=ObjectLevelWorkloadIdentity=true
1973+
```
18431974

18441975
#### PAT
18451976

internal/notifier/factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func googleChatNotifierFunc(opts notifierOptions) (Interface, error) {
263263
}
264264

265265
func googlePubSubNotifierFunc(opts notifierOptions) (Interface, error) {
266-
return NewGooglePubSub(opts.URL, opts.Channel, opts.Token, opts.Headers)
266+
return NewGooglePubSub(&opts)
267267
}
268268

269269
func webexNotifierFunc(opts notifierOptions) (Interface, error) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package notifier
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/url"
23+
24+
"google.golang.org/api/option"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
27+
"github.com/fluxcd/pkg/auth"
28+
"github.com/fluxcd/pkg/auth/gcp"
29+
"github.com/fluxcd/pkg/cache"
30+
31+
"github.com/fluxcd/notification-controller/api/v1beta3"
32+
)
33+
34+
// buildGCPClientOptions builds client options for GCP services.
35+
// Authentication precedence: JSON credentials take priority over workload identity.
36+
func buildGCPClientOptions(ctx context.Context, opts notifierOptions) ([]option.ClientOption, error) {
37+
var clientOpts []option.ClientOption
38+
39+
if opts.Token != "" {
40+
clientOpts = append(clientOpts, option.WithCredentialsJSON([]byte(opts.Token)))
41+
} else {
42+
var authOpts []auth.Option
43+
authOpts = append(authOpts, auth.WithClient(opts.TokenClient))
44+
45+
if opts.TokenCache != nil {
46+
involvedObject := cache.InvolvedObject{
47+
Kind: v1beta3.ProviderKind,
48+
Name: opts.ProviderName,
49+
Namespace: opts.ProviderNamespace,
50+
Operation: OperationPost,
51+
}
52+
authOpts = append(authOpts, auth.WithCache(*opts.TokenCache, involvedObject))
53+
}
54+
55+
if opts.ServiceAccountName != "" {
56+
serviceAccountKey := client.ObjectKey{
57+
Name: opts.ServiceAccountName,
58+
Namespace: opts.ProviderNamespace,
59+
}
60+
authOpts = append(authOpts, auth.WithServiceAccount(serviceAccountKey, opts.TokenClient))
61+
}
62+
63+
if opts.ProxyURL != "" {
64+
proxyURL, err := url.Parse(opts.ProxyURL)
65+
if err != nil {
66+
return nil, fmt.Errorf("error parsing proxy URL: %w", err)
67+
}
68+
authOpts = append(authOpts, auth.WithProxyURL(*proxyURL))
69+
}
70+
71+
tokenSource := gcp.NewTokenSource(ctx, authOpts...)
72+
clientOpts = append(clientOpts, option.WithTokenSource(tokenSource))
73+
}
74+
75+
return clientOpts, nil
76+
}

0 commit comments

Comments
 (0)