Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions api/v1/resourcesetinputprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,34 @@ const (
// +kubebuilder:validation:XValidation:rule="!has(self.serviceAccountName) || self.type.startsWith('AzureDevOps') || self.type.endsWith('ArtifactTag')", message="cannot specify spec.serviceAccountName when spec.type is not one of AzureDevOps* or *ArtifactTag"
// +kubebuilder:validation:XValidation:rule="!has(self.certSecretRef) || !(self.url == 'Static' || self.type.startsWith('AzureDevOps') || (self.type.endsWith('ArtifactTag') && self.type != 'OCIArtifactTag'))", message="cannot specify spec.certSecretRef when spec.type is one of Static, AzureDevOps*, ACRArtifactTag, ECRArtifactTag or GARArtifactTag"
// +kubebuilder:validation:XValidation:rule="!has(self.secretRef) || !(self.url == 'Static' || (self.type.endsWith('ArtifactTag') && self.type != 'OCIArtifactTag'))", message="cannot specify spec.secretRef when spec.type is one of Static, ACRArtifactTag, ECRArtifactTag or GARArtifactTag"
// +kubebuilder:validation:XValidation:rule="!has(self.credential) || (self.credential == 'ServiceAccountToken' && self.type == 'OCIArtifactTag')", message="spec.credential can be set to 'ServiceAccountToken' only when spec.type is 'OCIArtifactTag'"
// +kubebuilder:validation:XValidation:rule="!has(self.audiences) || size(self.audiences) == 0 || (has(self.credential) && self.credential == 'ServiceAccountToken')", message="spec.audiences can be set only when spec.credential is set to 'ServiceAccountToken'"
// +kubebuilder:validation:XValidation:rule="!has(self.credential) || self.credential != 'ServiceAccountToken' || (has(self.audiences) && size(self.audiences) > 0)", message="spec.audiences must be set when spec.credential is set to 'ServiceAccountToken'"
type ResourceSetInputProviderSpec struct {
// Type specifies the type of the input provider.
// +kubebuilder:validation:Enum=Static;GitHubBranch;GitHubTag;GitHubPullRequest;GitLabBranch;GitLabTag;GitLabMergeRequest;GitLabEnvironment;AzureDevOpsBranch;AzureDevOpsTag;AzureDevOpsPullRequest;OCIArtifactTag;ACRArtifactTag;ECRArtifactTag;GARArtifactTag
// +required
Type string `json:"type"`

// Credential specifies the type of credential that will be sent to the input provider.
// Supported values are:
//
// - ServiceAccountToken: The operator will generate a Kubernetes ServiceAccount
// token and send it as a bearer token to the provider. Currently supported only
// for the OCIArtifactTag type when connecting to OCI registries that support
// workload identity federation. If ServiceAccountName is not specified, the
// ServiceAccount of the operator will be used to generate the token.
//
// +kubebuilder:validation:Enum=ServiceAccountToken
// +optional
Credential string `json:"credential,omitempty"`

// Audiences specifies the audience claim to be set in JWT credentials,
// like the ServiceAccountToken credential. Required when using JWT
// credentials.
// +optional
Audiences []string `json:"audiences,omitempty"`

// URL specifies the HTTP/S or OCI address of the input provider API.
// When connecting to a Git provider, the URL should point to the repository address.
// When connecting to an OCI provider, the URL should point to the OCI repository address.
Expand All @@ -62,10 +84,13 @@ type ResourceSetInputProviderSpec struct {
URL string `json:"url,omitempty"`

// ServiceAccountName specifies the name of the Kubernetes ServiceAccount
// used for authentication with AWS, Azure or GCP services through
// workload identity federation features. If not specified, the
// authentication for these cloud providers will use the ServiceAccount
// of the operator (or any other environment authentication configuration).
// in the same namespace as the ResourceSetInputProvider used for
// authentication with workload identity federation features. If
// not specified, the authentication will use the ServiceAccount of the
// operator (or any other environment authentication configuration, e.g.
// for cloud providers). If Type is set to OCIArtifactTag and Credential
// is not set, the image pull secrets of the ServiceAccount are used to
// authenticate to the OCI registry.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ spec:
description: ResourceSetInputProviderSpec defines the desired state of
ResourceSetInputProvider
properties:
audiences:
description: |-
Audiences specifies the audience claim to be set in JWT credentials,
like the ServiceAccountToken credential. Required when using JWT
credentials.
items:
type: string
type: array
certSecretRef:
description: |-
CertSecretRef specifies the Kubernetes Secret containing either or both of
Expand All @@ -71,6 +79,19 @@ spec:
required:
- name
type: object
credential:
description: |-
Credential specifies the type of credential that will be sent to the input provider.
Supported values are:

- ServiceAccountToken: The operator will generate a Kubernetes ServiceAccount
token and send it as a bearer token to the provider. Currently supported only
for the OCIArtifactTag type when connecting to OCI registries that support
workload identity federation. If ServiceAccountName is not specified, the
ServiceAccount of the operator will be used to generate the token.
enum:
- ServiceAccountToken
type: string
defaultValues:
additionalProperties:
x-kubernetes-preserve-unknown-fields: true
Expand Down Expand Up @@ -177,10 +198,13 @@ spec:
serviceAccountName:
description: |-
ServiceAccountName specifies the name of the Kubernetes ServiceAccount
used for authentication with AWS, Azure or GCP services through
workload identity federation features. If not specified, the
authentication for these cloud providers will use the ServiceAccount
of the operator (or any other environment authentication configuration).
in the same namespace as the ResourceSetInputProvider used for
authentication with workload identity federation features. If
not specified, the authentication will use the ServiceAccount of the
operator (or any other environment authentication configuration, e.g.
for cloud providers). If Type is set to OCIArtifactTag and Credential
is not set, the image pull secrets of the ServiceAccount are used to
authenticate to the OCI registry.
type: string
skip:
description: Skip defines whether we need to skip input provider response
Expand Down Expand Up @@ -249,6 +273,17 @@ spec:
ACRArtifactTag, ECRArtifactTag or GARArtifactTag
rule: '!has(self.secretRef) || !(self.url == ''Static'' || (self.type.endsWith(''ArtifactTag'')
&& self.type != ''OCIArtifactTag''))'
- message: spec.credential can be set to 'ServiceAccountToken' only when
spec.type is 'OCIArtifactTag'
rule: '!has(self.credential) || (self.credential == ''ServiceAccountToken''
&& self.type == ''OCIArtifactTag'')'
- message: spec.audiences can be set only when spec.credential is set
to 'ServiceAccountToken'
rule: '!has(self.audiences) || size(self.audiences) == 0 || (has(self.credential)
&& self.credential == ''ServiceAccountToken'')'
- message: spec.audiences must be set when spec.credential is set to 'ServiceAccountToken'
rule: '!has(self.credential) || self.credential != ''ServiceAccountToken''
|| (has(self.audiences) && size(self.audiences) > 0)'
status:
description: ResourceSetInputProviderStatus defines the observed state
of ResourceSetInputProvider.
Expand Down
36 changes: 36 additions & 0 deletions docs/api/v1/resourcesetinputprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ The `.spec.serviceAccountName` field can only be used with the following provide
- `AzureDevOpsPullRequest`
- `AzureDevOpsBranch`
- `AzureDevOpsTag`
- `OCIArtifactTag` (when `.spec.credential` is set to `ServiceAccountToken`)
- `ACRArtifactTag`
- `ECRArtifactTag`
- `GARArtifactTag`
Expand All @@ -359,6 +360,41 @@ DevOps, [`.spec.secretRef`](#secret-based) is also not specified), the operator
to authenticate using the environment credentials, i.e. either the identity of the node or the
operator ServiceAccount. This is called *controller-level workload identity*.

##### ServiceAccountToken Credential for OCI Registries

For the `OCIArtifactTag` provider [type](#type), the `.spec.credential` field can be set
to `ServiceAccountToken` to enable authentication using a Kubernetes ServiceAccount token.
When this credential type is used, the operator generates a ServiceAccount token and sends
it as a bearer token to the OCI registry. This is useful for connecting to OCI registries
that support workload identity federation.

When using `ServiceAccountToken` credential, the `.spec.audiences` field is required and
specifies the audience claims to be included in the generated JWT token. The audience
values depend on the OCI registry provider being used.

If `.spec.serviceAccountName` is specified, the token is generated for that ServiceAccount.
Otherwise, the operator's own ServiceAccount is used to generate the token.

Example:

```yaml
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSetInputProvider
metadata:
name: oci-provider
namespace: default
spec:
type: OCIArtifactTag
url: oci://registry.example.com/my-repo
credential: ServiceAccountToken
audiences:
- "https://registry.example.com"
serviceAccountName: my-service-account # optional
filter:
semver: ">=1.0.0"
limit: 1
```

For configuring a Kubernetes ServiceAccount with workload identity, see the following documentation:

- [Azure](https://fluxcd.io/flux/integrations/azure/#with-workload-identity-federation)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/fluxcd/pkg/apis/event v0.21.0
github.com/fluxcd/pkg/apis/kustomize v1.14.0
github.com/fluxcd/pkg/apis/meta v1.24.0
github.com/fluxcd/pkg/auth v0.34.0
github.com/fluxcd/pkg/auth v0.34.1-0.20260118212638-6e3e8ddfe8fe
github.com/fluxcd/pkg/cache v0.12.0
github.com/fluxcd/pkg/git v0.40.0
github.com/fluxcd/pkg/kustomize v1.24.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ github.com/fluxcd/pkg/apis/kustomize v1.14.0 h1:PmWqMpRX0v7/aCAUNWfohe4o1qa9G3Cg
github.com/fluxcd/pkg/apis/kustomize v1.14.0/go.mod h1:CGRpU9Od4ht5+MHL6QlMfWaW87U9UTfGVM5CM4PZ28I=
github.com/fluxcd/pkg/apis/meta v1.24.0 h1:+e33T4OL9oqMWZSltsgImvi+/Punx42X9NqFlPesH6o=
github.com/fluxcd/pkg/apis/meta v1.24.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
github.com/fluxcd/pkg/auth v0.34.0 h1:lJFU7aW46vC6nOjpzQdnrz9XC1xXlaM5Z6d3qlozb8I=
github.com/fluxcd/pkg/auth v0.34.0/go.mod h1:BIz/zxLVz5o8EYQv+2c+ifAeaLq9wr4azXPdWYOU2AY=
github.com/fluxcd/pkg/auth v0.34.1-0.20260118212638-6e3e8ddfe8fe h1:NSz+6rUo31uy9owVgv8NCRbDNh48DQFOPEHVqUZTC5I=
github.com/fluxcd/pkg/auth v0.34.1-0.20260118212638-6e3e8ddfe8fe/go.mod h1:BIz/zxLVz5o8EYQv+2c+ifAeaLq9wr4azXPdWYOU2AY=
github.com/fluxcd/pkg/cache v0.12.0 h1:mabABT3jIfuo84VbIW+qvfqMZ7PbM5tXQgQvA2uo2rc=
github.com/fluxcd/pkg/cache v0.12.0/go.mod h1:HL/9cgBmwCdKIr3JH57rxrGdb7rOgX5Z1eJlHsaV1vE=
github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY=
Expand Down
27 changes: 18 additions & 9 deletions internal/controller/resourcesetinputprovider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/fluxcd/pkg/auth/aws"
"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/auth/gcp"
"github.com/fluxcd/pkg/auth/serviceaccounttoken"
authutils "github.com/fluxcd/pkg/auth/utils"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/git/github"
Expand Down Expand Up @@ -700,7 +701,8 @@ func (r *ResourceSetInputProviderReconciler) callOCIProvider(ctx context.Context
return res, nil
}

var inputProviderToCloudProvider = map[string]string{
var ociInputProviderToAuthProvider = map[string]string{
fluxcdv1.InputProviderOCIArtifactTag: serviceaccounttoken.CredentialName,
fluxcdv1.InputProviderACRArtifactTag: azure.ProviderName,
fluxcdv1.InputProviderECRArtifactTag: aws.ProviderName,
fluxcdv1.InputProviderGARArtifactTag: gcp.ProviderName,
Expand All @@ -719,12 +721,19 @@ func (r *ResourceSetInputProviderReconciler) buildOCIOptions(ctx context.Context
switch {

// Configure workload identity for cloud providers.
case obj.Spec.Type != fluxcdv1.InputProviderOCIArtifactTag:
case obj.Spec.Type != fluxcdv1.InputProviderOCIArtifactTag ||
obj.Spec.Credential == serviceaccounttoken.CredentialName:

authOpts := []auth.Option{
auth.WithClient(r.Client),
auth.WithServiceAccountNamespace(obj.GetNamespace()),
}

// Configure audiences.
if a := obj.Spec.Audiences; len(a) > 0 {
authOpts = append(authOpts, auth.WithAudiences(a...))
}

// Configure service account.
if s := obj.Spec.ServiceAccountName; s != "" {
authOpts = append(authOpts, auth.WithServiceAccountName(s))
Expand All @@ -737,7 +746,7 @@ func (r *ResourceSetInputProviderReconciler) buildOCIOptions(ctx context.Context
}

// Build authenticator.
provider := inputProviderToCloudProvider[obj.Spec.Type]
provider := ociInputProviderToAuthProvider[obj.Spec.Type]
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, repo, authOpts...)
if err != nil {
return nil, fmt.Errorf("failed to get artifact registry credentials for '%s', provider '%s': %w",
Expand Down Expand Up @@ -775,13 +784,13 @@ func (r *ResourceSetInputProviderReconciler) buildOCIOptions(ctx context.Context
}
opts = append(opts, crane.WithAuthFromKeychain(keychain))
}
}

// Configure TLS settings.
if tlsConfig != nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = tlsConfig
opts = append(opts, crane.WithTransport(transport))
}
// Configure TLS settings.
if tlsConfig != nil && obj.Spec.Type == fluxcdv1.InputProviderOCIArtifactTag {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = tlsConfig
opts = append(opts, crane.WithTransport(transport))
}

return opts, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"testing"

"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/auth/serviceaccounttoken"
"github.com/fluxcd/pkg/runtime/conditions"
kauth "github.com/google/go-containerregistry/pkg/authn/kubernetes"
"github.com/google/go-containerregistry/pkg/crane"
Expand Down Expand Up @@ -259,6 +261,89 @@ func TestResourceSetInputProviderReconciler_buildOCIOptions(t *testing.T) {
}
}

func TestResourceSetInputProviderReconciler_buildOCIOptions_ServiceAccountToken(t *testing.T) {
g := NewWithT(t)

r := getResourceSetInputProviderReconciler(t)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

ns, err := testEnv.CreateNamespace(ctx, "test-build-oci-options-sa-token")
g.Expect(err).NotTo(HaveOccurred())

// Create a ServiceAccount for the test.
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: ns.Name,
},
}
err = testEnv.Create(ctx, sa)
g.Expect(err).NotTo(HaveOccurred())

t.Run("uses serviceaccounttoken provider with serviceAccountName", func(t *testing.T) {
g := NewWithT(t)

// Enable the feature gate for ServiceAccountToken.
auth.EnableObjectLevelWorkloadIdentity()

obj := &fluxcdv1.ResourceSetInputProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: ns.Name,
},
Spec: fluxcdv1.ResourceSetInputProviderSpec{
Type: fluxcdv1.InputProviderOCIArtifactTag,
ServiceAccountName: "test-sa",
Credential: serviceaccounttoken.CredentialName,
Audiences: []string{"test-audience"},
},
}

const repo = "example.com/stefanprodan/podinfo"

opts, err := r.buildOCIOptions(ctx, obj, repo, nil, nil)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(opts).NotTo(BeNil())
})

t.Run("TLS config is applied with ServiceAccountToken credential", func(t *testing.T) {
g := NewWithT(t)

// Enable the feature gate for ServiceAccountToken.
auth.EnableObjectLevelWorkloadIdentity()

obj := &fluxcdv1.ResourceSetInputProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "test-tls",
Namespace: ns.Name,
},
Spec: fluxcdv1.ResourceSetInputProviderSpec{
Type: fluxcdv1.InputProviderOCIArtifactTag,
ServiceAccountName: "test-sa",
Credential: serviceaccounttoken.CredentialName,
Audiences: []string{"test-audience"},
},
}

const repo = "example.com/stefanprodan/podinfo"

tlsConfig := &tls.Config{
ServerName: "server.example.com",
}

opts, err := r.buildOCIOptions(ctx, obj, repo, tlsConfig, nil)
g.Expect(err).NotTo(HaveOccurred())

o := crane.GetOptions(opts...)

// Validate TLS config is set.
g.Expect(o.Transport).NotTo(BeNil())
g.Expect(o.Transport.(*http.Transport)).NotTo(BeNil())
g.Expect(o.Transport.(*http.Transport).TLSClientConfig.ServerName).To(Equal("server.example.com"))
})
}

func TestResourceSetInputProviderReconciler_InvalidOCIURL(t *testing.T) {
g := NewWithT(t)

Expand Down
Loading
Loading