Skip to content
Merged
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
37 changes: 37 additions & 0 deletions api/v1beta1/artifactgenerator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
AccessDeniedReason = "AccessDenied"
ValidationFailedReason = "ValidationFailed"
SourceFetchFailedReason = "SourceFetchFailed"
OwnershipConflictReason = "OwnershipConflict"
OverwriteStrategy = "Overwrite"
MergeStrategy = "Merge"
ExtractStrategy = "Extract"
Expand Down Expand Up @@ -69,6 +70,22 @@ type ArtifactGeneratorSpec struct {
// +required
Sources []SourceReference `json:"sources"`

// ServiceAccountName is the name of the ServiceAccount used to reconcile
// the generated ExternalArtifacts that target a namespace other than the
// ArtifactGenerator namespace. The ServiceAccount must exist in the
// ArtifactGenerator namespace. When specified, the controller impersonates
// this ServiceAccount for those ExternalArtifacts, and its RBAC bindings
// determine the namespaces in which they can be created, updated and
// deleted. ExternalArtifacts in the ArtifactGenerator namespace are always
// reconciled with the controller credentials.
// When not specified, the controller uses its own credentials, or the
// default ServiceAccount configured by the cluster administrator.
// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// PathPattern specifies a directory traversal pattern to match within the sources.
// The format is "@<alias>/<pattern>". Named captures in the pattern (e.g. "{app}")
// can be used as placeholders in OutputArtifacts fields.
Expand Down Expand Up @@ -125,6 +142,16 @@ type OutputArtifact struct {
// +required
Name string `json:"name"`

// Namespace is the namespace of the generated artifact.
// If not provided, defaults to the same namespace as the ArtifactGenerator.
// When set to a different namespace, the controller reconciles the artifact
// with the credentials of .spec.serviceAccountName or the controller default.
// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +optional
Namespace string `json:"namespace,omitempty"`

// Revision is the revision of the generated artifact.
// If specified, it must point to an existing source alias in the format "@<alias>".
// If not specified, the revision is automatically set to the digest of the artifact content.
Expand Down Expand Up @@ -266,6 +293,16 @@ func (in *ArtifactGenerator) IsDisabled() bool {
return ok && strings.ToLower(val) == DisabledValue
}

// GetArtifactNamespace returns the namespace where the ExternalArtifact
// generated for the given OutputArtifact is created. It defaults to the
// ArtifactGenerator namespace.
func (in *ArtifactGenerator) GetArtifactNamespace(outputArtifact *OutputArtifact) string {
if outputArtifact.Namespace != "" {
return outputArtifact.Namespace
}
return in.Namespace
}

// HasArtifactInInventory returns true if the artifact with the given
// kind, name, namespace, and digest exists in the inventory.
func (in *ArtifactGenerator) HasArtifactInInventory(name, namespace, digest string) bool {
Expand Down
36 changes: 36 additions & 0 deletions api/v1beta1/artifactgenerator_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2026 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 v1beta1_test

import (
"testing"

"github.com/fluxcd/source-watcher/api/v2/v1beta1"
)

func TestArtifactGeneratorGetArtifactNamespace(t *testing.T) {
obj := &v1beta1.ArtifactGenerator{}
obj.Namespace = "generator-ns"

if got := obj.GetArtifactNamespace(&v1beta1.OutputArtifact{}); got != "generator-ns" {
t.Errorf("GetArtifactNamespace() = %q, want %q", got, "generator-ns")
}

if got := obj.GetArtifactNamespace(&v1beta1.OutputArtifact{Namespace: "target-ns"}); got != "target-ns" {
t.Errorf("GetArtifactNamespace() = %q, want %q", got, "target-ns")
}
}
4 changes: 4 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func main() {
httpRetry int
reconciliationTimeout time.Duration
requeueDependency time.Duration
defaultServiceAccount string

// GitOps Toolkit (gotk) runtime options.
// https://pkg.go.dev/github.com/fluxcd/pkg/runtime
Expand All @@ -105,6 +106,8 @@ func main() {
"The maximum duration of a reconciliation.")
flag.DurationVar(&requeueDependency, "requeue-dependency", 5*time.Second,
"The interval at which failing dependencies are reevaluated.")
flag.StringVar(&defaultServiceAccount, "default-service-account", "",
"The default service account used for impersonation.")

aclOptions.BindFlags(flag.CommandLine)
artifactOptions.BindFlags(flag.CommandLine)
Expand Down Expand Up @@ -217,6 +220,7 @@ func main() {
DependencyRequeueInterval: requeueDependency,
DirectSourceFetch: directSourceFetch,
NoCrossNamespaceRefs: aclOptions.NoCrossNamespaceRefs,
DefaultServiceAccount: defaultServiceAccount,
}).SetupWithManager(ctx, mgr, controller.ArtifactGeneratorReconcilerOptions{
RateLimiter: gotkctrl.GetRateLimiter(rateLimiterOptions),
}); err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ spec:
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the generated artifact.
If not provided, defaults to the same namespace as the ArtifactGenerator.
When set to a different namespace, the controller reconciles the artifact
with the credentials of .spec.serviceAccountName or the controller default.
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
originRevision:
description: |-
OriginRevision is used to set the 'org.opencontainers.image.revision'
Expand Down Expand Up @@ -186,6 +196,22 @@ spec:
maxLength: 1024
pattern: ^@([a-z0-9]([a-z0-9_-]*[a-z0-9])?)/(.*)$
type: string
serviceAccountName:
description: |-
ServiceAccountName is the name of the ServiceAccount used to reconcile
the generated ExternalArtifacts that target a namespace other than the
ArtifactGenerator namespace. The ServiceAccount must exist in the
ArtifactGenerator namespace. When specified, the controller impersonates
this ServiceAccount for those ExternalArtifacts, and its RBAC bindings
determine the namespaces in which they can be created, updated and
deleted. ExternalArtifacts in the ArtifactGenerator namespace are always
reconciled with the controller credentials.
When not specified, the controller uses its own credentials, or the
default ServiceAccount configured by the cluster administrator.
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
sources:
description: |-
Sources is a list of references to the Flux source-controller
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ with advanced source composition and decomposition patterns.
| `--artifact-retention-records` | int | The maximum number of artifacts to be kept in storage after a garbage collection. (default 2) |
| `--artifact-retention-ttl` | duration | The duration of time that artifacts from previous reconciliations will be kept in storage before being garbage collected. (default 1m0s) |
| `--concurrent` | int | The number of concurrent reconciles per controller. (default 10) |
| `--default-service-account` | string | The default service account used for impersonation when `.spec.serviceAccountName` is not specified. |
| `--enable-leader-election` | boolean | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. |
| `--events-addr` | string | The address of the events receiver. |
| `--health-addr` | string | The address the health endpoint binds to. (default ":9440") |
Expand Down
53 changes: 53 additions & 0 deletions docs/spec/v1beta1/artifactgenerators.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ Each artifact must specify:

- `name` (required): The name of the generated ExternalArtifact resource. It must be unique in the context
of the ArtifactGenerator and must conform to Kubernetes resource naming conventions. Supports capture placeholders if `pathPattern` is used.
- `namespace` (optional): The namespace where the generated ExternalArtifact is created.
If not specified, it defaults to the ArtifactGenerator namespace. See
[Cross-namespace Artifacts](#cross-namespace-artifacts).
- `copy` (required): A list of copy operations to perform from sources to the artifact.
- `revision` (optional): A specific source revision to use in the format `@alias`.
If not specified, the revision is automatically computed as `latest@<digest>` based on the artifact content.
Expand Down Expand Up @@ -436,6 +439,55 @@ Any existing label or annotation on the generated resources will be overridden i
a common one. Note that the `app.kubernetes.io/managed-by` and `source.extensions.fluxcd.io/generator`
labels are reserved by the controller and cannot be overridden by common metadata.

### Cross-namespace Artifacts

By default, the generated ExternalArtifacts are created in the same namespace as the
ArtifactGenerator. The `.spec.artifacts[].namespace` field can be used to create an
ExternalArtifact in a different namespace. This is useful for multi-tenant clusters
where the sources and the ArtifactGenerator run in a shared namespace, while the
generated artifacts are consumed by tenants in their own namespaces.

The controller uses the ServiceAccount credentials only for artifacts whose
`.namespace` is set to a namespace different from the ArtifactGenerator namespace.
For artifacts in the ArtifactGenerator namespace (the default when `.namespace` is not
set), the controller always uses its own credentials, even when a ServiceAccount is
configured. This keeps the behavior of existing ArtifactGenerators unchanged.

For artifacts targeting another namespace, the controller impersonates the ServiceAccount
configured in `.spec.serviceAccountName`. The ServiceAccount must exist in the
ArtifactGenerator namespace, and its RBAC bindings determine which namespaces it can
access. When `.spec.serviceAccountName` is not specified, the controller uses its own
credentials.

For example, the following generator creates an ExternalArtifact in the `tenant-app`
namespace, using the `tenant-artifacts` ServiceAccount:

```yaml
apiVersion: source.extensions.fluxcd.io/v1beta1
kind: ArtifactGenerator
metadata:
name: tenant-app
namespace: flux-system
spec:
serviceAccountName: tenant-artifacts
sources:
- alias: repo
kind: GitRepository
name: my-monorepo
artifacts:
- name: tenant-app
namespace: tenant-app
copy:
- from: "@repo/tenants/tenant-app/**"
to: "@artifact/"
```

**Note** that on multi-tenant clusters, platform admins should configure a default
ServiceAccount for impersonation by starting the controller with the
`--default-service-account=<name>` flag. It is used whenever `.spec.serviceAccountName`
is not specified, and, like `.spec.serviceAccountName`, it only applies to artifacts
targeting a namespace different from the ArtifactGenerator namespace.

## Working with ArtifactGenerators

### Suspend and Resume Reconciliation
Expand Down Expand Up @@ -563,6 +615,7 @@ Events are emitted for the following scenarios:
- Build failures (e.g. invalid glob patterns, missing files).
- Storage operations (e.g. garbage collection, integrity validation failures).
- Drift detection (e.g. manual changes to generated ExternalArtifacts).
- Ownership conflicts (e.g. an ExternalArtifact generated by another ArtifactGenerator is taken over).

All events are also logged to the controller's standard output and contain
the ArtifactGenerator name and namespace.
Expand Down
Loading
Loading