1
0
mirror of synced 2026-03-01 19:26:55 +00:00

Compare commits

..

14 Commits

Author SHA1 Message Date
Stefan Prodan
4b4e6b1be3 Merge pull request #2382 from SomtochiAma/commit-sha
Use `client.Patch` for suspend/resume operations
2022-02-04 13:39:52 +02:00
Somtochi Onyekwere
d3d271defe use client.Patch for suspend/resume operations
Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>
2022-02-04 12:06:39 +01:00
Stefan Prodan
9bddabf4ff Merge pull request #2380 from souleb/fix-panic-orgref-var
Fix panic on bootstrap when orgRef is not retrieved
2022-02-04 10:29:26 +02:00
Soule BA
959ea6875a Fix panic on bootstrap when orgRef is not retrieved
If implemented, not retrieving an orgRef will always return an error

Signed-off-by: Soule BA <soule@weave.works>
2022-02-04 09:08:38 +01:00
Stefan Prodan
7b7eb011b0 Merge pull request #2377 from souleb/issue-2363
Fix `flux build/diff` when parsing SOPS encrypted secrets
2022-02-04 10:06:14 +02:00
Soule BA
997e6be3a2 Make sure to trim all sops data
If implemented this fixes #2363 and make sure we can build with sops
encrypted data

Signed-off-by: Soule BA <soule@weave.works>
2022-02-04 08:38:29 +01:00
Stefan Prodan
51af4bbf52 Merge pull request #2364 from robwittman/rwittman/add-github-gpg-signing
Add GPG signing to Github/Gitlab/Bitbucket bootstrap
2022-02-04 09:26:50 +02:00
Robert Wittman
e33198e750 Replace github boostrap GPG options
Signed-off-by: Robert Wittman <robkwittman@gmail.com>
2022-02-03 11:09:10 -05:00
Robert Wittman
e3f5a8fee3 Add GPG options to Gitlab and BitBucket bootstraps
Signed-off-by: Robert Wittman <robkwittman@gmail.com>
2022-02-03 11:07:55 -05:00
Robert Wittman
f8b58f8be9 Add GPG signing to Github bootstrap
Signed-off-by: Robert Wittman <robkwittman@gmail.com>
2022-02-03 11:03:35 -05:00
Stefan Prodan
55542a8086 Merge pull request #2376 from fluxcd/fix-azure-test
e2e: Fix Azure image update automation test
2022-02-03 17:04:01 +02:00
Stefan Prodan
70c8c0445c e2e: Fix Azure image update automation test
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2022-02-03 16:38:25 +02:00
Stefan Prodan
29c0bb4ce2 Merge pull request #2375 from souleb/issue-2365
Add contextual error code for flux diff kustomization
2022-02-03 16:35:45 +02:00
Soule BA
b86b195450 Add contextual error code for flux diff kustomization
If implemented, calling the diff command on kustomization will return 0,
1(if changes are identified), >1 for errors.

Signed-off-by: Soule BA <soule@weave.works>
2022-02-03 13:41:57 +01:00
15 changed files with 137 additions and 213 deletions

View File

@@ -254,6 +254,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithCABundle(caBundle),
bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))

View File

@@ -243,6 +243,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithCABundle(caBundle),
bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))

View File

@@ -257,6 +257,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithCABundle(caBundle),
bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))

View File

@@ -23,7 +23,7 @@ import (
var diffCmd = &cobra.Command{ var diffCmd = &cobra.Command{
Use: "diff", Use: "diff",
Short: "Diff a flux resource", Short: "Diff a flux resource",
Long: "The diff command is used to do a server-side dry-run on flux resources, then output the diff.", Long: "The diff command is used to do a server-side dry-run on flux resources, then prints the diff.",
} }
func init() { func init() {

View File

@@ -31,8 +31,9 @@ var diffKsCmd = &cobra.Command{
Use: "kustomization", Use: "kustomization",
Aliases: []string{"ks"}, Aliases: []string{"ks"},
Short: "Diff Kustomization", Short: "Diff Kustomization",
Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, Long: `The diff command does a build, then it performs a server-side dry-run and prints the diff.
Example: `# Preview changes local changes as they were applied on the cluster Exit status: 0 No differences were found. 1 Differences were found. >1 diff failed with an error.`,
Example: `# Preview local changes as they were applied on the cluster
flux diff kustomization my-app --path ./path/to/local/manifests`, flux diff kustomization my-app --path ./path/to/local/manifests`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: diffKsCmdRun, RunE: diffKsCmdRun,
@@ -56,16 +57,16 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
name := args[0] name := args[0]
if diffKsArgs.path == "" { if diffKsArgs.path == "" {
return fmt.Errorf("invalid resource path %q", diffKsArgs.path) return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)}
} }
if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() { if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() {
return fmt.Errorf("invalid resource path %q", diffKsArgs.path) return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)}
} }
builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout))
if err != nil { if err != nil {
return err return &RequestError{StatusCode: 2, Err: err}
} }
// create a signal channel // create a signal channel
@@ -74,13 +75,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
errChan := make(chan error) errChan := make(chan error)
go func() { go func() {
output, err := builder.Diff() output, hasChanged, err := builder.Diff()
if err != nil { if err != nil {
errChan <- err errChan <- &RequestError{StatusCode: 2, Err: err}
} }
cmd.Print(output) cmd.Print(output)
errChan <- nil
if hasChanged {
errChan <- &RequestError{StatusCode: 1, Err: fmt.Errorf("identified at least one change, exiting with non-zero exit code")}
} else {
errChan <- nil
}
}() }()
select { select {

View File

@@ -105,6 +105,16 @@ type rootFlags struct {
defaults install.Options defaults install.Options
} }
// RequestError is a custom error type that wraps an error returned by the flux api.
type RequestError struct {
StatusCode int
Err error
}
func (r *RequestError) Error() string {
return r.Err.Error()
}
var rootArgs = NewRootFlags() var rootArgs = NewRootFlags()
var kubeconfigArgs = genericclioptions.NewConfigFlags(false) var kubeconfigArgs = genericclioptions.NewConfigFlags(false)
@@ -144,6 +154,11 @@ func main() {
log.SetFlags(0) log.SetFlags(0)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
logger.Failuref("%v", err) logger.Failuref("%v", err)
if err, ok := err.(*RequestError); ok {
os.Exit(err.StatusCode)
}
os.Exit(1) os.Exit(1)
} }
} }

View File

@@ -325,6 +325,12 @@ type cmdTestCase struct {
func (cmd *cmdTestCase) runTestCmd(t *testing.T) { func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
actual, testErr := executeCommand(cmd.args) actual, testErr := executeCommand(cmd.args)
// If the cmd error is a change, discard it
if isChangeError(testErr) {
testErr = nil
}
if assertErr := cmd.assert(actual, testErr); assertErr != nil { if assertErr := cmd.assert(actual, testErr); assertErr != nil {
t.Error(assertErr) t.Error(assertErr)
} }
@@ -366,3 +372,12 @@ func resetCmdArgs() {
getArgs = GetFlags{} getArgs = GetFlags{}
secretGitArgs = NewSecretGitFlags() secretGitArgs = NewSecretGitFlags()
} }
func isChangeError(err error) bool {
if reqErr, ok := err.(*RequestError); ok {
if strings.Contains(err.Error(), "identified at least one change, exiting with non-zero exit code") && reqErr.StatusCode == 1 {
return true
}
}
return false
}

View File

@@ -48,6 +48,7 @@ func init() {
type resumable interface { type resumable interface {
adapter adapter
copyable
statusable statusable
setUnsuspended() setUnsuspended()
successMessage() string successMessage() string
@@ -97,10 +98,13 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error {
for i := 0; i < resume.list.len(); i++ { for i := 0; i < resume.list.len(); i++ {
logger.Actionf("resuming %s %s in %s namespace", resume.humanKind, resume.list.resumeItem(i).asClientObject().GetName(), *kubeconfigArgs.Namespace) logger.Actionf("resuming %s %s in %s namespace", resume.humanKind, resume.list.resumeItem(i).asClientObject().GetName(), *kubeconfigArgs.Namespace)
resume.list.resumeItem(i).setUnsuspended() obj := resume.list.resumeItem(i)
if err := kubeClient.Update(ctx, resume.list.resumeItem(i).asClientObject()); err != nil { patch := client.MergeFrom(obj.deepCopyClientObject())
obj.setUnsuspended()
if err := kubeClient.Patch(ctx, obj.asClientObject(), patch); err != nil {
return err return err
} }
logger.Successf("%s resumed", resume.humanKind) logger.Successf("%s resumed", resume.humanKind)
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{

View File

@@ -46,6 +46,7 @@ func init() {
type suspendable interface { type suspendable interface {
adapter adapter
copyable
isSuspended() bool isSuspended() bool
setSuspended() setSuspended()
} }
@@ -94,8 +95,11 @@ func (suspend suspendCommand) run(cmd *cobra.Command, args []string) error {
for i := 0; i < suspend.list.len(); i++ { for i := 0; i < suspend.list.len(); i++ {
logger.Actionf("suspending %s %s in %s namespace", suspend.humanKind, suspend.list.item(i).asClientObject().GetName(), *kubeconfigArgs.Namespace) logger.Actionf("suspending %s %s in %s namespace", suspend.humanKind, suspend.list.item(i).asClientObject().GetName(), *kubeconfigArgs.Namespace)
suspend.list.item(i).setSuspended()
if err := kubeClient.Update(ctx, suspend.list.item(i).asClientObject()); err != nil { obj := suspend.list.item(i)
patch := client.MergeFrom(obj.deepCopyClientObject())
obj.setSuspended()
if err := kubeClient.Patch(ctx, obj.asClientObject(), patch); err != nil {
return err return err
} }
logger.Successf("%s suspended", suspend.humanKind) logger.Successf("%s suspended", suspend.humanKind)

View File

@@ -275,7 +275,7 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g
subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName)
orgRef, err := b.getOrganization(ctx, subOrgs) orgRef, err := b.getOrganization(ctx, subOrgs)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create new Git repository for the organization %q: %w", orgRef.String(), err) return nil, fmt.Errorf("failed to create new Git repository %q: %w", b.repositoryName, err)
} }
repoRef := newOrgRepositoryRef(*orgRef, repoName) repoRef := newOrgRepositoryRef(*orgRef, repoName)
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility) repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)

View File

@@ -36,6 +36,7 @@ import (
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
const ( const (
@@ -262,16 +263,30 @@ func trimSopsData(res *resource.Resource) error {
if res.GetKind() == "Secret" { if res.GetKind() == "Secret" {
dataMap := res.GetDataMap() dataMap := res.GetDataMap()
for k, v := range dataMap { asYaml, err := res.AsYAML()
data, err := base64.StdEncoding.DecodeString(v) if err != nil {
if err != nil { return fmt.Errorf("failed to decode secret %s data: %w", res.GetName(), err)
if _, ok := err.(base64.CorruptInputError); ok { }
return fmt.Errorf("failed to decode secret data: %w", err)
} //delete any sops data as we don't want to expose it
if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
res.PipeE(yaml.FieldClearer{Name: "sops"})
for k := range dataMap {
dataMap[k] = sopsMess
} }
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { } else {
dataMap[k] = sopsMess for k, v := range dataMap {
data, err := base64.StdEncoding.DecodeString(v)
if err != nil {
if _, ok := err.(base64.CorruptInputError); ok {
return fmt.Errorf("failed to decode secret %s data: %w", res.GetName(), err)
}
}
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
dataMap[k] = sopsMess
}
} }
} }

View File

@@ -91,6 +91,45 @@ kind: Secret
metadata: metadata:
name: secret-basic-auth name: secret-basic-auth
type: kubernetes.io/basic-auth type: kubernetes.io/basic-auth
`,
},
{
name: "secret sops secret",
yamlStr: `apiVersion: v1
data:
.dockercfg: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str]
kind: Secret
metadata:
name: secret
type: kubernetes.io/dockerconfigjson
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eU1CTEJhVXZ4eEVYYkVV
OU90TEcrR2pYckttN0pBanJoSUZWSW1RQXlRCkUydFJ3V1NZUTBuVFF0aC9GUEcw
bUdhNjJWTkoyL1FUVi9Dc1dxUDBkM0UKLS0tIE1sQXkwcWdGaEFuY0RHQTVXM0J6
dWpJcThEbW15V3dXYXpPZklBdW1Hd1kKoIAdmGNPrEctV8h1w8KuvQ5S+BGmgqN9
MgpNmUhJjWhgcQpb5BRYpQesBOgU5TBGK7j58A6DMDKlSiYZsdQchQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2022-02-03T16:03:17Z"
mac: ENC[AES256_GCM,data:AHdYSawajwgAFwlmDN1IPNmT9vWaYKzyVIra2d6sPcjTbZ8/p+VRSRpVm4XZFFsaNnW5AUJaouwXnKYDTmJDXKlr/rQcu9kXqsssQgdzcXaA6l5uJlgsnml8ba7J3OK+iEKMax23mwQEx2EUskCd9ENOwFDkunP02sxqDNOz20k=,iv:8F5OamHt3fAVorf6p+SoIrWoqkcATSGWVoM0EK87S4M=,tag:E1mxXnc7wWkEX5BxhpLtng==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.1
`,
expected: `apiVersion: v1
data:
.dockercfg: KipTT1BTKio=
kind: Secret
metadata:
name: secret
type: kubernetes.io/dockerconfigjson
`, `,
}, },
} }

View File

@@ -51,28 +51,29 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) {
return ssa.NewResourceManager(b.client, statusPoller, owner), nil return ssa.NewResourceManager(b.client, statusPoller, owner), nil
} }
func (b *Builder) Diff() (string, error) { func (b *Builder) Diff() (string, bool, error) {
output := strings.Builder{} output := strings.Builder{}
createdOrDrifted := false
res, err := b.Build() res, err := b.Build()
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
// convert the build result into Kubernetes unstructured objects // convert the build result into Kubernetes unstructured objects
objects, err := ssa.ReadObjects(bytes.NewReader(res)) objects, err := ssa.ReadObjects(bytes.NewReader(res))
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
resourceManager, err := b.Manager() resourceManager, err := b.Manager()
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
ctx, cancel := context.WithTimeout(context.Background(), b.timeout) ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel() defer cancel()
if err := ssa.SetNativeKindsDefaults(objects); err != nil { if err := ssa.SetNativeKindsDefaults(objects); err != nil {
return "", err return "", createdOrDrifted, err
} }
// create an inventory of objects to be reconciled // create an inventory of objects to be reconciled
@@ -101,20 +102,23 @@ func (b *Builder) Diff() (string, error) {
if change.Action == string(ssa.CreatedAction) { if change.Action == string(ssa.CreatedAction) {
output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green))
createdOrDrifted = true
} }
if change.Action == string(ssa.ConfiguredAction) { if change.Action == string(ssa.ConfiguredAction) {
output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke)) output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke))
liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject)
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
defer cleanupDir(tmpDir) defer cleanupDir(tmpDir)
err = diff(liveFile, mergedFile, &output) err = diff(liveFile, mergedFile, &output)
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
createdOrDrifted = true
} }
addObjectsToInventory(newInventory, change) addObjectsToInventory(newInventory, change)
@@ -125,7 +129,7 @@ func (b *Builder) Diff() (string, error) {
if oldStatus.Inventory != nil { if oldStatus.Inventory != nil {
diffObjects, err := diffInventory(oldStatus.Inventory, newInventory) diffObjects, err := diffInventory(oldStatus.Inventory, newInventory)
if err != nil { if err != nil {
return "", err return "", createdOrDrifted, err
} }
for _, object := range diffObjects { for _, object := range diffObjects {
output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed)) output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed))
@@ -133,7 +137,7 @@ func (b *Builder) Diff() (string, error) {
} }
} }
return output.String(), nil return output.String(), createdOrDrifted, nil
} }
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {

View File

@@ -1,181 +0,0 @@
# RFC-0002 Access control for source references
**Status:** provisional
**Creation date:** 2021-11-16
**Last update:** 2022-02-03
## Summary
Cross-namespace references to Flux sources should be subject to
Access Control Lists (ACLs) as defined by the owner of a particular source.
Similar to [Kubernetes Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/),
Flux ACLs define policies for restricting the access to the source artifact server based on the
caller's namespace.
## Motivation
As of [version 0.26](https://github.com/fluxcd/flux2/releases/tag/v0.26.0) (Feb 2022),
Flux allows for `Kustomizations`, `HelmReleases` and `ImageUpdateAutomations` to reference sources in different namespaces.
On multi-tenant clusters, platform admins can disable this behaviour with the `--no-cross-namespace-refs` flag
as described in the [multi-tenancy lockdown documentation](https://fluxcd.io/docs/installation/#multi-tenancy-lockdown).
This proposal tries to solve the "cross-namespace references side-step namespace isolation" issue (explained in
[RFC-0001](https://github.com/fluxcd/flux2/tree/main/rfcs/0001-authorization#cross-namespace-references-side-step-namespace-isolation))
for when platform admins want to allow tenants to share sources.
### Goals
- Allow source owners to choose which sources are shared and with which namespaces.
- Allow cluster admins to enforce source ACLs.
### Non-Goals
- Enforce source ACLs by default.
## Proposal
Extend the current Image Policy/Repository ACL implementation to all the others Flux resources
as described in [flux2#1704](https://github.com/fluxcd/flux2/issues/1704).
When a Flux resource (`Kustomization`, `HelmRelease` or `ImageUpdateAutomation`)
refers to a source (`GitRepository`, `HelmRepository` or `Bucket`) in a different namespace,
access is granted based on the source ACL.
The ACL check is performed only if `--enable-source-acl` flag is set to `true` for the following controllers:
- kustomize-controller
- helm-controller
- image-automation-controller
### User Stories
#### Story 1
> As a cluster admin, I want to share Helm Repositories approved by the platform team with all tenants.
If the owner of a Flux `HelmRepository` wants to grant access to the repository for all namespaces in a cluster,
an empty `matchLabels` can be used:
```yaml
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
name: bitnami
namespace: flux-system
spec:
url: https://charts.bitnami.com/bitnami
accessFrom:
namespaceSelectors:
- matchLabels: {}
```
If the `accessFrom` field is not present and `--enable-source-acl` is set to `true`,
means that a source can't be accessed from any other namespace but the one where it currently resides.
#### Story 2
> As a tenant, I want to share my app repository with another tenant
> so that they can deploy the application in their own namespace.
If `dev-team1` wants to grant read access to their repository to `dev-team2`,
a `matchLabels` that selects the namespace owned by `dev-team2` can be used:
```yaml
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: app1
namespace: dev-team1
spec:
url: ssh://git@github.com/<org>/app1-deploy
secretRef:
name: app1-ro-ssh-key
accessFrom:
namespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: dev-team2
```
#### Story 3
> As a cluster admin, I want to let tenants configure image automation in their namespaces by
> referring to a Git repository managed by the platform team.
If the owner of a Flux `GitRepository` wants to grant write access to `ImageUpdateAutomations` in a different namespace,
a `matchLabels` that selects the image automation namespace can be used:
```yaml
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: cluster-config
namespace: flux-system
spec:
url: ssh://git@github.com/<org>/cluster-config
secretRef:
name: read-write-ssh-key
accessFrom:
namespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: dev-team1
```
The `dev-team1` can refer to the `cluster-config` repository in their image automation config:
```yaml
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: app1
namespace: dev-team1
spec:
sourceRef:
kind: GitRepository
name: cluster-config
namespace: flux-system
```
### Alternatives
#### Admission controllers
An alternative solution to source ACLs is to use an admission controller such as Kyverno or OPA Gatekeeper
and allow/disallow cross-namespace access to specific source.
The current proposal offers the same feature but without the need to manage yet another controller to guard
sources.
#### Kubernetes RBAC
Another alternative is to rely on impersonation and create a `ClusterRoleBinding` per named source and tenant account
as described in [fluxcd/flux2#582](https://github.com/fluxcd/flux2/pull/582).
The current proposal is more flexible than RBAC and implies less work for Flux users. ALCs act more like
Kubernetes Network Policies where access is defined based on labels, with RBAC every time a namespace is added,
the platform admins have to create new RBAC rules to target that namespace.
#### Source reflection CRD
Yet another alternative is to introduce a new API kind `SourceReflection` as described in
[fluxcd/flux2#582-821027543](https://github.com/fluxcd/flux2/pull/582#issuecomment-821027543).
The current proposal allows the owner to define the access control list on the source object, instead
of creating objects in namespaces where it has no control over.
#### Remove cross-namespace refs
An alternative is to simply remove cross-namespace references from the Flux API.
This would break with current behavior, and users would have to make substantial changes to their
repository structure and workflow. In cases where e.g. a resource is common (across many namespaces),
this would mean the source-controller would use way more memory and network bandwidth that grows with
each namespace that uses the same Git or Helm repository due to the requirement of having to duplicate
"common" resources.
## Implementation History
- ACL support for allowing cross-namespace access to `ImageRepositories` was first released in flux2 **v0.23.0**.
- Disabling cross-namespace access to sources was first released in flux2 **v0.26.0**.

View File

@@ -483,7 +483,7 @@ func TestImageRepositoryACR(t *testing.T) {
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: 1 * time.Minute, Duration: 1 * time.Minute,
}, },
SourceRef: automationv1beta1.SourceReference{ SourceRef: automationv1beta1.CrossNamespaceSourceReference{
Kind: "GitRepository", Kind: "GitRepository",
Name: name, Name: name,
}, },