Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b4e6b1be3 | ||
|
|
d3d271defe | ||
|
|
9bddabf4ff | ||
|
|
959ea6875a | ||
|
|
7b7eb011b0 | ||
|
|
997e6be3a2 | ||
|
|
51af4bbf52 | ||
|
|
e33198e750 | ||
|
|
e3f5a8fee3 | ||
|
|
f8b58f8be9 | ||
|
|
55542a8086 | ||
|
|
70c8c0445c | ||
|
|
29c0bb4ce2 | ||
|
|
b86b195450 |
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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**.
|
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user