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

Compare commits

..

23 Commits

Author SHA1 Message Date
Stefan Prodan
a0520de7aa Merge pull request #2397 from fluxcd/ssa-v0.13.0
Fix bootstrap CRD wait race condition
2022-02-07 14:59:05 +02:00
Stefan Prodan
4602b72778 Fix bootstrap CRD wait race condition
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2022-02-07 14:28:56 +02:00
Stefan Prodan
e69a6ed91a Merge pull request #2398 from fluxcd/update-components
Update toolkit components
2022-02-07 14:28:20 +02:00
Stefan Prodan
9d6a037935 Update dependencies
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2022-02-07 14:09:23 +02:00
fluxcdbot
41df03f600 Update toolkit components
- kustomize-controller to v0.20.1
  https://github.com/fluxcd/kustomize-controller/blob/v0.20.1/CHANGELOG.md
- source-controller to v0.21.2
  https://github.com/fluxcd/source-controller/blob/v0.21.2/CHANGELOG.md

Signed-off-by: GitHub <noreply@github.com>
2022-02-07 11:45:14 +00:00
Stefan Prodan
ca92464ef6 Merge pull request #2392 from souleb/issue-2387
Mask dockerconfigjson secret types and support StringData secrets
2022-02-07 11:18:11 +02:00
Soule BA
2e9fd33ce5 Mask dockerconfigjson secret types and support StringData secrets
If implemented, flux diff kustomization will managed correctly sops
managed dockerconfigjson secrets.
Sops encrypted secret with stringData maps are supported too.

Signed-off-by: Soule BA <soule@weave.works>
2022-02-07 09:45:38 +01:00
Stefan Prodan
cf3f729f98 Merge pull request #2389 from souleb/fix-deleted-mess-diff
Fix wrong deletion message on flux diff
2022-02-07 10:09:51 +02:00
Soule BA
8b444283e6 Fix wrong deletion message on flux diff
If implemented, when an error happens when dry-running an object, we
return early. This match pkg ssa implementation

Signed-off-by: Soule BA <soule@weave.works>
2022-02-07 00:06:33 +01:00
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
38 changed files with 431 additions and 276 deletions

View File

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

View File

@@ -23,7 +23,7 @@ import (
var diffCmd = &cobra.Command{
Use: "diff",
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() {

View File

@@ -31,8 +31,9 @@ var diffKsCmd = &cobra.Command{
Use: "kustomization",
Aliases: []string{"ks"},
Short: "Diff Kustomization",
Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`,
Example: `# Preview changes local changes as they were applied on the cluster
Long: `The diff command does a build, then it performs a server-side dry-run and prints the diff.
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`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: diffKsCmdRun,
@@ -56,16 +57,16 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
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() {
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))
if err != nil {
return err
return &RequestError{StatusCode: 2, Err: err}
}
// create a signal channel
@@ -74,13 +75,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
errChan := make(chan error)
go func() {
output, err := builder.Diff()
output, hasChanged, err := builder.Diff()
if err != nil {
errChan <- err
errChan <- &RequestError{StatusCode: 2, Err: err}
}
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 {

View File

@@ -79,6 +79,18 @@ func TestDiffKustomization(t *testing.T) {
objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"),
},
{
name: "diff with a sops dockerconfigjson secret object",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo",
objectFile: "./testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden"),
},
{
name: "diff with a sops stringdata secret object",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo",
objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-stringdata-sops-secret.golden"),
},
}
tmpl := map[string]string{

View File

@@ -105,6 +105,16 @@ type rootFlags struct {
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 kubeconfigArgs = genericclioptions.NewConfigFlags(false)
@@ -143,6 +153,17 @@ func NewRootFlags() rootFlags {
func main() {
log.SetFlags(0)
if err := rootCmd.Execute(); err != nil {
if err, ok := err.(*RequestError); ok {
if err.StatusCode == 1 {
logger.Warningf("%v", err)
} else {
logger.Failuref("%v", err)
}
os.Exit(err.StatusCode)
}
logger.Failuref("%v", err)
os.Exit(1)
}

View File

@@ -325,6 +325,12 @@ type cmdTestCase struct {
func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
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 {
t.Error(assertErr)
}
@@ -366,3 +372,12 @@ func resetCmdArgs() {
getArgs = GetFlags{}
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 {
adapter
copyable
statusable
setUnsuspended()
successMessage() string
@@ -97,10 +98,13 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error {
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)
resume.list.resumeItem(i).setUnsuspended()
if err := kubeClient.Update(ctx, resume.list.resumeItem(i).asClientObject()); err != nil {
obj := resume.list.resumeItem(i)
patch := client.MergeFrom(obj.deepCopyClientObject())
obj.setUnsuspended()
if err := kubeClient.Patch(ctx, obj.asClientObject(), patch); err != nil {
return err
}
logger.Successf("%s resumed", resume.humanKind)
namespacedName := types.NamespacedName{

View File

@@ -46,6 +46,7 @@ func init() {
type suspendable interface {
adapter
copyable
isSuspended() bool
setSuspended()
}
@@ -94,8 +95,11 @@ func (suspend suspendCommand) run(cmd *cobra.Command, args []string) error {
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)
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
}
logger.Successf("%s suspended", suspend.humanKind)

View File

@@ -123,6 +123,31 @@ spec:
type: ClusterIP
---
apiVersion: v1
data:
.dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: docker-secret
namespace: default
type: kubernetes.io/dockerconfigjson
---
apiVersion: v1
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: secret-basic-auth-stringdata
namespace: default
stringData:
password: KipTT1BTKio=
username: KipTT1BTKio=
type: kubernetes.io/basic-auth
---
apiVersion: v1
data:
token: KipTT1BTKio=
kind: Secret

View File

@@ -0,0 +1,27 @@
apiVersion: v1
data:
.dockerconfigjson: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str]
kind: Secret
metadata:
name: docker-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

View File

@@ -4,6 +4,8 @@ resources:
- ./deployment.yaml
- ./hpa.yaml
- ./service.yaml
- ./dockerconfigjson-sops-secret.yaml
- ./stringdata-secret.yaml
secretGenerator:
- files:
- token=token.encrypted

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth-stringdata
type: kubernetes.io/basic-auth
stringData:
username: ENC[AES256_GCM,data:uKiQR48=,iv:jh2lgyAVu7igJAgoJsnOGhjxFyvUAa9lvT21u3hhqpU=,tag:zXM2JEpk3ZEH7WfkcWXXkw==,type:str]
password: ENC[AES256_GCM,data:PyhZmNhy929JGQ==,iv:PBqPaJmSw21+kn4gIlg5VdjLNZyf613z5RUTCesBoVw=,tag:Hjc7DsuUrtsz7PYPdNkL3g==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJd0xxbDZhYjVoZzY4YWhK
d2NvMVgrSGRVUGhHRGg3R1FpVURnbmh1TDBzCjcwby85M3JaK09QVk0yZFNMb2NL
c2NQZW5hS1FhYlBHU0VoUzBVYzZYUUUKLS0tIEdaNEw2Y0VjVHpZc3pyYUtLVmJk
NmN3K2VLU0NiZ1d0VHBYbGlCM1lrNmMKeWz3yfFbMNE+ly21oLfc1XnDSPRmnlPP
wIs8lk/qrzVZ45C9GdWnnPeGZZiia46Yop9TxseUS8gCjJ6KCxJCAg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2022-02-06T12:51:07Z"
mac: ENC[AES256_GCM,data:jtdzwj19uxdxvnmXg1HkAkDA6XlKMJOYFy7uLI5t/t11LwGop5Yeo7a4nQEEELehRx9J7B6U6NiySxAxBxWx5uW5vI5c8+069VV6dkiCIefnYSzuoIhQafjlFl1/KvH7VEjIWfHYuXF09v9PEKXkxEHUYDpS3QqQ3ymHRRI08pU=, iv:xX3E7F+AM29Pm8G5oqxRfYu9E7tEBGIaHeCJYgrtFmc=,tag:MJPGusNvu05z939jg8PAwQ==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.1

View File

@@ -1,4 +1,6 @@
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -0,0 +1,6 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -1,6 +1,8 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 drifted
data

View File

@@ -1,6 +1,8 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c drifted

View File

@@ -7,5 +7,7 @@ spec.ports.http.port
- 9899
+ 9898
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -1,4 +1,6 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -0,0 +1,6 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -0,0 +1,11 @@
apiVersion: v1
data:
.dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: docker-secret
namespace: default
type: kubernetes.io/dockerconfigjson

View File

@@ -1,5 +1,7 @@
► Deployment/default/podinfo created
► HorizontalPodAutoscaler/default/podinfo created
► Service/default/podinfo created
► Secret/default/docker-secret created
► Secret/default/secret-basic-auth-stringdata created
► Secret/default/podinfo-token-77t89m9b67 created
► Secret/default/db-user-pass-bkbd782d2c created

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: secret-basic-auth-stringdata
namespace: default
stringData:
password: KipTT1BTKio=
username: KipTT1BTKio=
type: kubernetes.io/basic-auth

View File

@@ -11,6 +11,6 @@ spec:
branch: main
secretRef:
name: flux-system
timeout: 20s
timeout: 1m0s
url: ssh://git@github.com/example/repo

8
go.mod
View File

@@ -10,23 +10,23 @@ require (
github.com/fluxcd/helm-controller/api v0.16.0
github.com/fluxcd/image-automation-controller/api v0.20.0
github.com/fluxcd/image-reflector-controller/api v0.16.0
github.com/fluxcd/kustomize-controller/api v0.20.0
github.com/fluxcd/kustomize-controller/api v0.20.1
github.com/fluxcd/notification-controller/api v0.21.0
github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect
github.com/fluxcd/pkg/apis/meta v0.10.2
github.com/fluxcd/pkg/kustomize v0.0.2
github.com/fluxcd/pkg/runtime v0.12.4
github.com/fluxcd/pkg/ssa v0.12.0
github.com/fluxcd/pkg/ssa v0.13.0
github.com/fluxcd/pkg/ssh v0.3.1
github.com/fluxcd/pkg/untar v0.0.5
github.com/fluxcd/pkg/version v0.0.1
github.com/fluxcd/source-controller/api v0.21.1
github.com/fluxcd/source-controller/api v0.21.2
github.com/go-git/go-git/v5 v5.4.2
github.com/gonvenience/bunt v1.3.2
github.com/gonvenience/ytbx v1.4.2
github.com/google/go-cmp v0.5.6
github.com/google/go-containerregistry v0.2.0
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/homeport/dyff v1.4.6
github.com/lucasb-eyer/go-colorful v1.2.0

12
go.sum
View File

@@ -230,8 +230,8 @@ github.com/fluxcd/image-automation-controller/api v0.20.0 h1:Z+lxqif0KwccsuNOBZq
github.com/fluxcd/image-automation-controller/api v0.20.0/go.mod h1:XhLYccGUbmJvTTpJ1jAFKZHr2e1GNXy0T85ZBO50mik=
github.com/fluxcd/image-reflector-controller/api v0.16.0 h1:1O1YdoK7LsJgWLyvfZTSbvQcUQCBcgJ573HA0arlQQY=
github.com/fluxcd/image-reflector-controller/api v0.16.0/go.mod h1:OIe3mSXc3OwQiNbiQ9vNXWYtNif31hc7WAbZWlFUUnc=
github.com/fluxcd/kustomize-controller/api v0.20.0 h1:Vw+2qCxeHMv0y1mfiBgVrMfcfFevBMrRfLEdfPTrb40=
github.com/fluxcd/kustomize-controller/api v0.20.0/go.mod h1:5MdpzJVx8+KiDIRv37zLme992BAOCgE0v1n+NOgs1lo=
github.com/fluxcd/kustomize-controller/api v0.20.1 h1:BMOUdKCb6bZ6kepcxU8vz9VcDbl4q6SH9j/mRPl2O1A=
github.com/fluxcd/kustomize-controller/api v0.20.1/go.mod h1:5MdpzJVx8+KiDIRv37zLme992BAOCgE0v1n+NOgs1lo=
github.com/fluxcd/notification-controller/api v0.21.0 h1:D5B3TH5YtSww0SyvW1Ru5xWsh0MgHQanC/a1t3CvXq0=
github.com/fluxcd/notification-controller/api v0.21.0/go.mod h1:gA9/j0kjh7VDuUC2Cubr9twxOdzb/0+ojcE9Lzsc9ug=
github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc=
@@ -245,8 +245,8 @@ github.com/fluxcd/pkg/kustomize v0.0.2/go.mod h1:AFwnf3OqQmpTCuwCARTGpPRMBf0ZFJN
github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM=
github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0=
github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc=
github.com/fluxcd/pkg/ssa v0.12.0 h1:7nF4UigU9Zk/9P/nbzUP3ah8IRC+BpB64O9iu5VnvEo=
github.com/fluxcd/pkg/ssa v0.12.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA=
github.com/fluxcd/pkg/ssa v0.13.0 h1:LU4wf7dB9ksYdda0BEWNTBSTd68E5YwWxuPiPLAtw4Y=
github.com/fluxcd/pkg/ssa v0.13.0/go.mod h1:XGVGjUaG152HGN6sSUj+VFK/Th5i5rj2XsXSDdlIMNU=
github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244=
github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ=
github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk=
@@ -254,8 +254,8 @@ github.com/fluxcd/pkg/untar v0.0.5/go.mod h1:O6V9+rtl8c1mHBafgqFlJN6zkF1HS5SSYn7
github.com/fluxcd/pkg/version v0.0.1 h1:/8asQoDXSThz3csiwi4Qo8Zb6blAxLXbtxNgeMJ9bCg=
github.com/fluxcd/pkg/version v0.0.1/go.mod h1:WAF4FEEA9xyhngF8TDxg3UPu5fA1qhEYV8Pmi2Il01Q=
github.com/fluxcd/source-controller/api v0.21.0/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/fluxcd/source-controller/api v0.21.1 h1:7X39YQHmB1vmIBrHxU+YAqxwtdC9Zh+tdtMKREW3xiQ=
github.com/fluxcd/source-controller/api v0.21.1/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/fluxcd/source-controller/api v0.21.2 h1:J0S5NN4V8FPLrkSMXIUoUvj1X/RuTpVJSjIRF414wmc=
github.com/fluxcd/source-controller/api v0.21.2/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=

View File

@@ -275,7 +275,7 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g
subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName)
orgRef, err := b.getOrganization(ctx, subOrgs)
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)
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)

View File

@@ -20,6 +20,7 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"sync"
"time"
@@ -36,12 +37,17 @@ import (
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const (
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
mask = "**SOPS**"
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
mask = "**SOPS**"
dockercfgSecretType = "kubernetes.io/dockerconfigjson"
typeField = "type"
dataField = "data"
stringDataField = "stringData"
)
var defaultTimeout = 80 * time.Second
@@ -182,7 +188,7 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
}
// make sure secrets are masked
err = trimSopsData(res)
err = maskSopsData(res)
if err != nil {
return
}
@@ -256,26 +262,131 @@ func (b *Builder) setOwnerLabels(res *resource.Resource) error {
return nil
}
func trimSopsData(res *resource.Resource) error {
func maskSopsData(res *resource.Resource) error {
// sopsMess is the base64 encoded mask
sopsMess := base64.StdEncoding.EncodeToString([]byte(mask))
if res.GetKind() == "Secret" {
// get both data and stringdata maps as a secret can have both
dataMap := res.GetDataMap()
for k, v := range dataMap {
data, err := base64.StdEncoding.DecodeString(v)
stringDataMap := getStringDataMap(res)
asYaml, err := res.AsYAML()
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
// delete any sops data as we don't want to expose it
// assume that both data and stringdata are encrypted
if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
// delete the sops object
res.PipeE(yaml.FieldClearer{Name: "sops"})
secretType, err := res.GetFieldValue(typeField)
if err != nil {
if _, ok := err.(base64.CorruptInputError); ok {
return fmt.Errorf("failed to decode secret data: %w", err)
}
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
dataMap[k] = sopsMess
if v, ok := secretType.(string); ok && v == dockercfgSecretType {
// if the secret is a json docker config secret, we need to mask the data with a json object
err := maskDockerconfigjsonSopsData(dataMap)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
err = maskDockerconfigjsonSopsData(stringDataMap)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
} else {
for k := range dataMap {
dataMap[k] = sopsMess
}
for k := range stringDataMap {
stringDataMap[k] = sopsMess
}
}
} else {
err := maskBase64EncryptedSopsData(dataMap, sopsMess)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
err = maskSopsDataInStringDataSecret(stringDataMap, sopsMess)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
}
// set the data and stringdata maps
res.SetDataMap(dataMap)
if len(stringDataMap) > 0 {
err = res.SetMapField(yaml.NewMapRNode(&stringDataMap), stringDataField)
if err != nil {
return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
}
}
}
return nil
}
func getStringDataMap(rn *resource.Resource) map[string]string {
n, err := rn.Pipe(yaml.Lookup(stringDataField))
if err != nil {
return nil
}
result := map[string]string{}
_ = n.VisitFields(func(node *yaml.MapNode) error {
result[yaml.GetValue(node.Key)] = yaml.GetValue(node.Value)
return nil
})
return result
}
func maskDockerconfigjsonSopsData(dataMap map[string]string) error {
sopsMess := struct {
Mask string `json:"mask"`
}{
Mask: mask,
}
maskJson, err := json.Marshal(sopsMess)
if err != nil {
return err
}
for k := range dataMap {
dataMap[k] = base64.StdEncoding.EncodeToString(maskJson)
}
return nil
}
func maskBase64EncryptedSopsData(dataMap map[string]string, mask string) error {
for k, v := range dataMap {
data, err := base64.StdEncoding.DecodeString(v)
if err != nil {
if _, ok := err.(base64.CorruptInputError); ok {
return err
}
}
if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
dataMap[k] = mask
}
}
return nil
}
func maskSopsDataInStringDataSecret(stringDataMap map[string]string, mask string) error {
for k, v := range stringDataMap {
if bytes.Contains([]byte(v), []byte("sops")) && bytes.Contains([]byte(v), []byte("ENC[")) {
stringDataMap[k] = mask
}
}
return nil

View File

@@ -91,6 +91,45 @@ kind: Secret
metadata:
name: secret-basic-auth
type: kubernetes.io/basic-auth
`,
},
{
name: "secret sops secret",
yamlStr: `apiVersion: v1
data:
.dockerconfigjson: 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:
.dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ==
kind: Secret
metadata:
name: secret
type: kubernetes.io/dockerconfigjson
`,
},
}
@@ -103,7 +142,7 @@ type: kubernetes.io/basic-auth
}
resource := &resource.Resource{RNode: *r}
err = trimSopsData(resource)
err = maskSopsData(resource)
if err != nil {
t.Fatalf("unable to trim sops data: %v", err)
}

View File

@@ -32,6 +32,7 @@ import (
"github.com/gonvenience/bunt"
"github.com/gonvenience/ytbx"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-multierror"
"github.com/homeport/dyff/pkg/dyff"
"github.com/lucasb-eyer/go-colorful"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -51,30 +52,32 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) {
return ssa.NewResourceManager(b.client, statusPoller, owner), nil
}
func (b *Builder) Diff() (string, error) {
func (b *Builder) Diff() (string, bool, error) {
output := strings.Builder{}
createdOrDrifted := false
res, err := b.Build()
if err != nil {
return "", err
return "", createdOrDrifted, err
}
// convert the build result into Kubernetes unstructured objects
objects, err := ssa.ReadObjects(bytes.NewReader(res))
if err != nil {
return "", err
return "", createdOrDrifted, err
}
resourceManager, err := b.Manager()
if err != nil {
return "", err
return "", createdOrDrifted, err
}
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
if err := ssa.SetNativeKindsDefaults(objects); err != nil {
return "", err
return "", createdOrDrifted, err
}
var diffErrs error
// create an inventory of objects to be reconciled
newInventory := newInventory()
for _, obj := range objects {
@@ -85,11 +88,8 @@ func (b *Builder) Diff() (string, error) {
}
change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions)
if err != nil {
if b.kustomization.Spec.Force && ssa.IsImmutableError(err) {
output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green))
} else {
output.WriteString(writeString(fmt.Sprintf("✗ %v\n", err), bunt.Red))
}
// gather errors and continue, as we want to see all the diffs
diffErrs = multierror.Append(diffErrs, err)
continue
}
@@ -101,31 +101,34 @@ func (b *Builder) Diff() (string, error) {
if change.Action == string(ssa.CreatedAction) {
output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green))
createdOrDrifted = true
}
if change.Action == string(ssa.ConfiguredAction) {
output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke))
liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject)
if err != nil {
return "", err
return "", createdOrDrifted, err
}
defer cleanupDir(tmpDir)
err = diff(liveFile, mergedFile, &output)
if err != nil {
return "", err
return "", createdOrDrifted, err
}
createdOrDrifted = true
}
addObjectsToInventory(newInventory, change)
}
if b.kustomization.Spec.Prune {
if b.kustomization.Spec.Prune && diffErrs == nil {
oldStatus := b.kustomization.Status.DeepCopy()
if oldStatus.Inventory != nil {
diffObjects, err := diffInventory(oldStatus.Inventory, newInventory)
if err != nil {
return "", err
return "", createdOrDrifted, err
}
for _, object := range diffObjects {
output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed))
@@ -133,7 +136,7 @@ func (b *Builder) Diff() (string, error) {
}
}
return output.String(), nil
return output.String(), createdOrDrifted, diffErrs
}
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {
@@ -196,17 +199,31 @@ func diff(liveFile, mergedFile string, output io.Writer) error {
}
func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
data := obj.Object["data"]
for _, v := range data.(map[string]interface{}) {
// get both data and stringdata maps
data := obj.Object[dataField]
stringData := obj.Object[stringDataField]
if m, ok := data.(map[string]interface{}); ok && m != nil {
applySopsDiff(m, liveObject, mergedObject, change)
}
if m, ok := stringData.(map[string]interface{}); ok && m != nil {
applySopsDiff(m, liveObject, mergedObject, change)
}
}
func applySopsDiff(data map[string]interface{}, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) {
for _, v := range data {
v, err := base64.StdEncoding.DecodeString(v.(string))
if err != nil {
fmt.Println(err)
}
if bytes.Contains(v, []byte(mask)) {
if liveObject != nil && mergedObject != nil {
change.Action = string(ssa.UnchangedAction)
dataLive := liveObject.Object["data"].(map[string]interface{})
dataMerged := mergedObject.Object["data"].(map[string]interface{})
dataLive := liveObject.Object[dataField].(map[string]interface{})
dataMerged := mergedObject.Object[dataField].(map[string]interface{})
if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" {
change.Action = string(ssa.ConfiguredAction)
}

View File

@@ -1,8 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.0/kustomize-controller.crds.yaml
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.0/kustomize-controller.deployment.yaml
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.1/kustomize-controller.crds.yaml
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.1/kustomize-controller.deployment.yaml
- account.yaml
patchesJson6902:
- target:

View File

@@ -1,8 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/fluxcd/source-controller/releases/download/v0.21.1/source-controller.crds.yaml
- https://github.com/fluxcd/source-controller/releases/download/v0.21.1/source-controller.deployment.yaml
- https://github.com/fluxcd/source-controller/releases/download/v0.21.2/source-controller.crds.yaml
- https://github.com/fluxcd/source-controller/releases/download/v0.21.2/source-controller.deployment.yaml
- account.yaml
patchesJson6902:
- target:

View File

@@ -1,8 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/fluxcd/source-controller/releases/download/v0.21.1/source-controller.crds.yaml
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.0/kustomize-controller.crds.yaml
- https://github.com/fluxcd/source-controller/releases/download/v0.21.2/source-controller.crds.yaml
- https://github.com/fluxcd/kustomize-controller/releases/download/v0.20.1/kustomize-controller.crds.yaml
- https://github.com/fluxcd/helm-controller/releases/download/v0.16.0/helm-controller.crds.yaml
- https://github.com/fluxcd/notification-controller/releases/download/v0.21.0/notification-controller.crds.yaml
- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.16.0/image-reflector-controller.crds.yaml

View File

@@ -1,206 +0,0 @@
# RFC-0003 Flux Multi-Tenancy Mode
**Status:** provisional
**Creation date:** 2021-11-16
**Last update:** 2022-02-03
## Summary
For multi-tenant environments, we want to offer an easy way of configuring Flux to enforce tenant isolation
(as defined by the Soft Multi-Tenancy model from RFC-0001).
When running in the multi-tenant mode, Flux will lock down access to sources (as defined by RFC-0002),
and will use the tenant service account instead of defaulting to `cluster-admin`.
From an end-user perspective, the multi-tenancy mode means that:
- Platform admins have to create a Kubernetes service account and RBAC in each namespace where
Flux performs source-to-cluster reconciliation on behalf of tenants.
By default, Flux will have no permissions to reconcile the tenants sources onto clusters.
- Source owners have to specify with which tenants they wish to share their sources.
By default, nothing is shared between tenants.
## Motivation
As of [version 0.26](https://github.com/fluxcd/flux2/releases/tag/v0.26.0) (Feb 2022),
configuring Flux for soft multi-tenancy requires platform admins to:
- Deny cross-namespace access to Flux custom resources by setting the `--no-cross-namespace-refs` flag.
- Enforce impersonation by setting a default service account with the `--default-service-account` flag.
Instead of using a Kustomize patch to lock down Flux as descried in the
[multi-tenancy lockdown documentation](https://fluxcd.io/docs/installation/#multi-tenancy-lockdown),
we could extend `flux install` and `flux bootstrap` and offer a flag to configure Flux with multi-tenancy enforcements.
### Goals
- Enforce service account impersonation for source-to-cluster reconciliation.
- Enforce ACLs for cross-namespace access to sources.
### Non-Goals
- Enforce tenant's workload isolation with network policies and pod security standards as described
[here](https://kubernetes.io/blog/2021/04/15/three-tenancy-models-for-kubernetes/#security-considerations).
## Proposal
### User Stories
#### Story 1
> As a platform admin, I want to install Flux with lowest privilege/permission level possible.
#### Story 2
> As a platform admin, I want to give tenants full control over their assigned namespaces.
> So that tenants could use their own repositories and manager the app delivery with Flux.
#### Story 3
> As a platform admin, I want to prevent tenants from changing the cluster-wide configuration.
> If a tenant adds to their repository a cluster-scoped resource such as a namespace or cluster role,
> Flux should reject the change and notify the tenant that this operation is not allowed.
### Multi-tenant Bootstrap
When bootstrapping Flux, platform admins should have the option to lock down Flux for multi-tenant environments e.g.:
```shell
flux bootstrap --security-profile=multi-tenant
```
The security profile flag accepts two values: `single-tenant` and `multi-tenant`.
Platform admins may switch between the two modes at any time, either by rerunning bootstrap
or by patching the Flux manifests in Git.
The `multi-tenant` profile is just a shortcut to setting the following container args in the Flux deployment manifests:
```yaml
containers:
- name: manager
args:
- --default-service-account=flux
- --enable-source-acl=true
```
And for disabling cross-namespace references when using the notification API:
```yaml
kind: Deployment
metadata:
name: notification-controller
spec:
template:
spec:
containers:
- name: manager
args:
- --no-cross-namespace-refs=true
```
When running in the `multi-tenant` mode, Flux behaves differently:
- The source-to-cluster reconciliation no longer runs under the service account of
the Flux controllers. The controller service account, is only used to impersonate
the service account specified in the Flux custom resources (`Kustomizations`, `HelmReleases`).
- When no service account name is specified in a Flux custom resource,
a default will be used e.g. `system:serviceaccount:<tenant-namespace>:flux`.
- When a Flux custom resource (`Kustomizations`, `HelmReleases`, `ImagePolicies`, `ImageUpdateAutomations`)
refers to a source in a different namespace, access is granted based the source access control list.
If no ACL is defined for a source, cross-namespace access is denied.
- When a Flux notification (`Alerts`, `Receivers`)
refers to a resource in a different namespace, access is denied.
### Tenants Onboarding
When onboarding tenants, platform admins should have the option to assign namespaces, set
permissions and register the tenants repositories onto clusters in a declarative manner.
The Flux CLI offers an easy way of generating all the Kubernetes manifests needed to onboard tenants:
- `flux create tenant` command generates namespaces, service accounts and Kubernetes RBAC
with restricted access to the cluster resources, given tenants access only to their namespaces.
- `flux create secret git` command generates SSH keys used by Flux to clone the tenants repositories.
- `flux create source git` command generates the configuration that tells Flux which repositories belong to tenants.
- `flux create kustomization` command generates the configuration that tells Flux how to reconcile the manifests found in the tenants repositories.
All the above commands have an `--export` flag for generating the Kubernetes resources in YAML format.
The platform admins should place the generated manifests in the repository that defines the cluster(s) desired state.
Here is an example of the generated manifests:
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: tenant1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: flux
namespace: tenant1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: flux
namespace: tenant1
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: flux
namespace: tenant1
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: tenant1
namespace: tenant1
spec:
interval: 5m0s
ref:
branch: main
secretRef:
name: tenant1-git-auth
url: ssh://git@github.com/org/tenant1
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: tenant1
namespace: tenant1
spec:
interval: 10m0s
path: ./
prune: true
serviceAccountName: flux
sourceRef:
kind: GitRepository
name: tenant1
```
Note that the [cluster-admin](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
role is used in a `RoleBinding`, this only gives full control over every resource in the role binding's namespace.
Once the tenants repositories are registered on the cluster(s), the tenants can configure their app delivery
in Git using Kubernetes namespace-scoped resources such as `Deployments`, `Services`, Flagger `Canaries`,
Flux `Kustomizations`, `HelmReleases`, `ImageUpdateAutomations`, `Alerts`, `Receivers`, etc.
## Alternatives
Instead of introducing the security profile flag to `flux bootstrap`,
we could document how to patch each controller deployment with Kustomize as described in the
[multi-tenancy lockdown documentation](https://fluxcd.io/docs/installation/#multi-tenancy-lockdown).
Having an easy way of locking down Flux with a single flag, make users aware of the security implications
and improves the user experience.
## Implementation History
- Disabling cross-namespace access and providing a default service account was first released in flux2 **v0.26.0**.

View File

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

View File

@@ -7,11 +7,11 @@ require (
github.com/fluxcd/helm-controller/api v0.16.0
github.com/fluxcd/image-automation-controller/api v0.20.0
github.com/fluxcd/image-reflector-controller/api v0.16.0
github.com/fluxcd/kustomize-controller/api v0.20.0
github.com/fluxcd/kustomize-controller/api v0.20.1
github.com/fluxcd/notification-controller/api v0.21.0
github.com/fluxcd/pkg/apis/meta v0.10.2
github.com/fluxcd/pkg/runtime v0.12.4
github.com/fluxcd/source-controller/api v0.21.1
github.com/fluxcd/source-controller/api v0.21.2
github.com/hashicorp/terraform-exec v0.14.0
github.com/libgit2/git2go/v31 v31.6.1
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5

View File

@@ -204,8 +204,8 @@ github.com/fluxcd/image-automation-controller/api v0.20.0 h1:Z+lxqif0KwccsuNOBZq
github.com/fluxcd/image-automation-controller/api v0.20.0/go.mod h1:XhLYccGUbmJvTTpJ1jAFKZHr2e1GNXy0T85ZBO50mik=
github.com/fluxcd/image-reflector-controller/api v0.16.0 h1:1O1YdoK7LsJgWLyvfZTSbvQcUQCBcgJ573HA0arlQQY=
github.com/fluxcd/image-reflector-controller/api v0.16.0/go.mod h1:OIe3mSXc3OwQiNbiQ9vNXWYtNif31hc7WAbZWlFUUnc=
github.com/fluxcd/kustomize-controller/api v0.20.0 h1:Vw+2qCxeHMv0y1mfiBgVrMfcfFevBMrRfLEdfPTrb40=
github.com/fluxcd/kustomize-controller/api v0.20.0/go.mod h1:5MdpzJVx8+KiDIRv37zLme992BAOCgE0v1n+NOgs1lo=
github.com/fluxcd/kustomize-controller/api v0.20.1 h1:BMOUdKCb6bZ6kepcxU8vz9VcDbl4q6SH9j/mRPl2O1A=
github.com/fluxcd/kustomize-controller/api v0.20.1/go.mod h1:5MdpzJVx8+KiDIRv37zLme992BAOCgE0v1n+NOgs1lo=
github.com/fluxcd/notification-controller/api v0.21.0 h1:D5B3TH5YtSww0SyvW1Ru5xWsh0MgHQanC/a1t3CvXq0=
github.com/fluxcd/notification-controller/api v0.21.0/go.mod h1:gA9/j0kjh7VDuUC2Cubr9twxOdzb/0+ojcE9Lzsc9ug=
github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc=
@@ -218,8 +218,8 @@ github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5f
github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0=
github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc=
github.com/fluxcd/source-controller/api v0.21.0/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/fluxcd/source-controller/api v0.21.1 h1:7X39YQHmB1vmIBrHxU+YAqxwtdC9Zh+tdtMKREW3xiQ=
github.com/fluxcd/source-controller/api v0.21.1/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/fluxcd/source-controller/api v0.21.2 h1:J0S5NN4V8FPLrkSMXIUoUvj1X/RuTpVJSjIRF414wmc=
github.com/fluxcd/source-controller/api v0.21.2/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=