Repository: fluxcd/flux2 Branch: main Commit: d9f51d047dbb Files: 667 Total size: 1.7 MB Directory structure: gitextract_vsl03042/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ └── config.yml │ ├── aur/ │ │ ├── flux-bin/ │ │ │ ├── .SRCINFO.template │ │ │ ├── .gitignore │ │ │ ├── PKGBUILD.template │ │ │ └── publish.sh │ │ ├── flux-go/ │ │ │ ├── .SRCINFO.template │ │ │ ├── .gitignore │ │ │ ├── PKGBUILD.template │ │ │ └── publish.sh │ │ └── flux-scm/ │ │ ├── .SRCINFO.template │ │ ├── .gitignore │ │ ├── PKGBUILD.template │ │ └── publish.sh │ ├── dependabot.yml │ ├── kind/ │ │ └── config.yaml │ ├── labels.yaml │ ├── runners/ │ │ ├── README.md │ │ ├── prereq.sh │ │ └── runner-setup.sh │ └── workflows/ │ ├── README.md │ ├── action.yaml │ ├── backport.yaml │ ├── conformance.yaml │ ├── e2e-azure.yaml │ ├── e2e-bootstrap.yaml │ ├── e2e-gcp.yaml │ ├── e2e.yaml │ ├── ossf.yaml │ ├── release.yaml │ ├── scan.yaml │ ├── sync-labels.yaml │ ├── update.yaml │ └── upgrade-fluxcd-pkg.yaml ├── .gitignore ├── .goreleaser.yml ├── .scorecard.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DCO ├── Dockerfile ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── action/ │ ├── README.md │ └── action.yml ├── cmd/ │ └── flux/ │ ├── alert.go │ ├── alert_provider.go │ ├── artifact.go │ ├── bootstrap.go │ ├── bootstrap_bitbucket_server.go │ ├── bootstrap_git.go │ ├── bootstrap_gitea.go │ ├── bootstrap_github.go │ ├── bootstrap_gitlab.go │ ├── build.go │ ├── build_artifact.go │ ├── build_artifact_test.go │ ├── build_kustomization.go │ ├── build_kustomization_test.go │ ├── check.go │ ├── check_test.go │ ├── cluster_info.go │ ├── cluster_info_test.go │ ├── completion.go │ ├── completion_bash.go │ ├── completion_fish.go │ ├── completion_powershell.go │ ├── completion_zsh.go │ ├── create.go │ ├── create_alert.go │ ├── create_alertprovider.go │ ├── create_helmrelease.go │ ├── create_helmrelease_test.go │ ├── create_image.go │ ├── create_image_policy.go │ ├── create_image_repository.go │ ├── create_image_update.go │ ├── create_kustomization.go │ ├── create_receiver.go │ ├── create_secret.go │ ├── create_secret_git.go │ ├── create_secret_git_test.go │ ├── create_secret_github_app.go │ ├── create_secret_githubapp_test.go │ ├── create_secret_helm.go │ ├── create_secret_helm_test.go │ ├── create_secret_notation.go │ ├── create_secret_notation_test.go │ ├── create_secret_oci.go │ ├── create_secret_oci_test.go │ ├── create_secret_proxy.go │ ├── create_secret_proxy_test.go │ ├── create_secret_tls.go │ ├── create_secret_tls_test.go │ ├── create_source.go │ ├── create_source_bucket.go │ ├── create_source_chart.go │ ├── create_source_chart_test.go │ ├── create_source_git.go │ ├── create_source_git_test.go │ ├── create_source_helm.go │ ├── create_source_helm_test.go │ ├── create_source_oci.go │ ├── create_source_oci_test.go │ ├── create_tenant.go │ ├── create_tenant_test.go │ ├── create_test.go │ ├── debug.go │ ├── debug_helmrelease.go │ ├── debug_helmrelease_test.go │ ├── debug_kustomization.go │ ├── debug_kustomization_test.go │ ├── delete.go │ ├── delete_alert.go │ ├── delete_alertprovider.go │ ├── delete_helmrelease.go │ ├── delete_image.go │ ├── delete_image_policy.go │ ├── delete_image_repository.go │ ├── delete_image_update.go │ ├── delete_kustomization.go │ ├── delete_receiver.go │ ├── delete_source.go │ ├── delete_source_bucket.go │ ├── delete_source_chart.go │ ├── delete_source_git.go │ ├── delete_source_helm.go │ ├── delete_source_oci.go │ ├── diff.go │ ├── diff_artifact.go │ ├── diff_artifact_test.go │ ├── diff_kustomization.go │ ├── diff_kustomization_test.go │ ├── docgen.go │ ├── envsubst.go │ ├── envsubst_test.go │ ├── events.go │ ├── events_test.go │ ├── export.go │ ├── export_alert.go │ ├── export_alertprovider.go │ ├── export_artifact.go │ ├── export_artifact_generator.go │ ├── export_helmrelease.go │ ├── export_image.go │ ├── export_image_policy.go │ ├── export_image_repository.go │ ├── export_image_update.go │ ├── export_kustomization.go │ ├── export_receiver.go │ ├── export_secret.go │ ├── export_source.go │ ├── export_source_bucket.go │ ├── export_source_chart.go │ ├── export_source_external.go │ ├── export_source_git.go │ ├── export_source_helm.go │ ├── export_source_oci.go │ ├── export_test.go │ ├── get.go │ ├── get_alert.go │ ├── get_alertprovider.go │ ├── get_all.go │ ├── get_artifact.go │ ├── get_artifact_generator.go │ ├── get_helmrelease.go │ ├── get_image.go │ ├── get_image_all.go │ ├── get_image_policy.go │ ├── get_image_repository.go │ ├── get_image_update.go │ ├── get_kustomization.go │ ├── get_receiver.go │ ├── get_source.go │ ├── get_source_all.go │ ├── get_source_bucket.go │ ├── get_source_chart.go │ ├── get_source_external.go │ ├── get_source_git.go │ ├── get_source_helm.go │ ├── get_source_oci.go │ ├── get_test.go │ ├── helmrelease.go │ ├── helmrelease_test.go │ ├── image.go │ ├── image_test.go │ ├── install.go │ ├── install_test.go │ ├── kustomization.go │ ├── kustomization_test.go │ ├── list.go │ ├── list_artifact.go │ ├── log.go │ ├── logs.go │ ├── logs_e2e_test.go │ ├── logs_unit_test.go │ ├── main.go │ ├── main_e2e_test.go │ ├── main_test.go │ ├── main_unit_test.go │ ├── manifests.embed.go │ ├── migrate.go │ ├── migrate_test.go │ ├── object.go │ ├── oci.go │ ├── pull.go │ ├── pull_artifact.go │ ├── push.go │ ├── push_artifact.go │ ├── readiness.go │ ├── readiness_test.go │ ├── receiver.go │ ├── reconcile.go │ ├── reconcile_helmrelease.go │ ├── reconcile_image.go │ ├── reconcile_image_policy.go │ ├── reconcile_image_repository.go │ ├── reconcile_image_updateauto.go │ ├── reconcile_kustomization.go │ ├── reconcile_receiver.go │ ├── reconcile_source.go │ ├── reconcile_source_bucket.go │ ├── reconcile_source_chart.go │ ├── reconcile_source_git.go │ ├── reconcile_source_helm.go │ ├── reconcile_source_oci.go │ ├── reconcile_with_source.go │ ├── resume.go │ ├── resume_alert.go │ ├── resume_alertprovider.go │ ├── resume_helmrelease.go │ ├── resume_image.go │ ├── resume_image_policy.go │ ├── resume_image_repository.go │ ├── resume_image_updateauto.go │ ├── resume_kustomization.go │ ├── resume_receiver.go │ ├── resume_source.go │ ├── resume_source_bucket.go │ ├── resume_source_chart.go │ ├── resume_source_git.go │ ├── resume_source_helm.go │ ├── resume_source_oci.go │ ├── source.go │ ├── source_oci_test.go │ ├── stats.go │ ├── status.go │ ├── suspend.go │ ├── suspend_alert.go │ ├── suspend_alertprovider.go │ ├── suspend_helmrelease.go │ ├── suspend_image.go │ ├── suspend_image_policy.go │ ├── suspend_image_repository.go │ ├── suspend_image_updateauto.go │ ├── suspend_kustomization.go │ ├── suspend_receiver.go │ ├── suspend_source.go │ ├── suspend_source_bucket.go │ ├── suspend_source_chart.go │ ├── suspend_source_git.go │ ├── suspend_source_helm.go │ ├── suspend_source_oci.go │ ├── tag.go │ ├── tag_artifact.go │ ├── testdata/ │ │ ├── build-kustomization/ │ │ │ ├── delete-service/ │ │ │ │ ├── deployment.yaml │ │ │ │ ├── hpa.yaml │ │ │ │ └── kustomization.yaml │ │ │ ├── ignore/ │ │ │ │ ├── .sourceignore │ │ │ │ ├── configmap.yaml │ │ │ │ ├── not_deployable/ │ │ │ │ │ └── ignore_svc.yaml │ │ │ │ └── secret.yaml │ │ │ ├── my-app/ │ │ │ │ └── configmap.yaml │ │ │ ├── podinfo/ │ │ │ │ ├── deployment.yaml │ │ │ │ ├── dockerconfigjson-sops-secret.yaml │ │ │ │ ├── hpa.yaml │ │ │ │ ├── kustomization.yaml │ │ │ │ ├── service.yaml │ │ │ │ ├── stringdata-secret.yaml │ │ │ │ └── token.encrypted │ │ │ ├── podinfo-kustomization.yaml │ │ │ ├── podinfo-result.yaml │ │ │ ├── podinfo-source.yaml │ │ │ ├── podinfo-with-ignore-result.yaml │ │ │ ├── podinfo-with-my-app/ │ │ │ │ ├── kustomization.yaml │ │ │ │ └── my-app.yaml │ │ │ ├── podinfo-with-my-app-result.yaml │ │ │ ├── podinfo-with-var-substitution-result.yaml │ │ │ ├── podinfo-without-service-result.yaml │ │ │ └── var-substitution/ │ │ │ ├── cluster.json │ │ │ ├── deployment.yaml │ │ │ └── kustomization.yaml │ │ ├── check/ │ │ │ └── check_pre.golden │ │ ├── cluster_info/ │ │ │ └── gitrepositories.yaml │ │ ├── create_hr/ │ │ │ ├── basic.yaml │ │ │ ├── hc_basic.yaml │ │ │ ├── or_basic.yaml │ │ │ └── setup-source.yaml │ │ ├── create_secret/ │ │ │ ├── git/ │ │ │ │ ├── ecdsa-password.private │ │ │ │ ├── ecdsa.private │ │ │ │ ├── git-bearer-token.yaml │ │ │ │ ├── git-ssh-secret-password.yaml │ │ │ │ ├── git-ssh-secret.yaml │ │ │ │ ├── secret-ca-crt.yaml │ │ │ │ └── secret-git-basic.yaml │ │ │ ├── githubapp/ │ │ │ │ ├── secret-with-baseurl.yaml │ │ │ │ ├── secret.yaml │ │ │ │ └── test-private-key.pem │ │ │ ├── helm/ │ │ │ │ └── secret-helm.yaml │ │ │ ├── notation/ │ │ │ │ ├── invalid-trust-policy.json │ │ │ │ ├── invalid.json │ │ │ │ ├── secret-ca-crt.yaml │ │ │ │ ├── secret-ca-multi.yaml │ │ │ │ ├── secret-ca-pem.yaml │ │ │ │ ├── test-ca.crt │ │ │ │ ├── test-ca2.crt │ │ │ │ └── test-trust-policy.json │ │ │ ├── oci/ │ │ │ │ └── create-secret.yaml │ │ │ ├── proxy/ │ │ │ │ └── secret-proxy.yaml │ │ │ └── tls/ │ │ │ ├── secret-tls.yaml │ │ │ ├── test-ca.pem │ │ │ ├── test-cert.pem │ │ │ └── test-key.pem │ │ ├── create_source_chart/ │ │ │ ├── basic.yaml │ │ │ ├── setup-source.yaml │ │ │ ├── verify_basic.yaml │ │ │ └── verify_complete.yaml │ │ ├── create_source_git/ │ │ │ ├── export.golden │ │ │ ├── source-git-branch-commit.yaml │ │ │ ├── source-git-branch.yaml │ │ │ ├── source-git-commit.yaml │ │ │ ├── source-git-provider-azure.yaml │ │ │ ├── source-git-provider-generic.yaml │ │ │ ├── source-git-provider-github.yaml │ │ │ ├── source-git-refname.yaml │ │ │ ├── source-git-semver.yaml │ │ │ ├── source-git-tag.yaml │ │ │ └── success.golden │ │ ├── create_source_helm/ │ │ │ ├── https.golden │ │ │ ├── oci-with-secret.golden │ │ │ └── oci.golden │ │ ├── create_tenant/ │ │ │ ├── tenant-basic.yaml │ │ │ ├── tenant-with-cluster-role.yaml │ │ │ ├── tenant-with-service-account.yaml │ │ │ └── tenant-with-skip-namespace.yaml │ │ ├── debug_helmrelease/ │ │ │ ├── history-empty.golden.yaml │ │ │ ├── history.golden.yaml │ │ │ ├── objects.yaml │ │ │ ├── status.golden.yaml │ │ │ ├── values-from.golden.yaml │ │ │ └── values-inline.golden.yaml │ │ ├── debug_kustomization/ │ │ │ ├── history-empty.golden.yaml │ │ │ ├── history.golden.yaml │ │ │ ├── objects.yaml │ │ │ ├── status.golden.yaml │ │ │ ├── vars-from.golden.env │ │ │ └── vars.golden.env │ │ ├── diff-artifact/ │ │ │ ├── deployment-diff.yaml │ │ │ ├── deployment.yaml │ │ │ └── success.golden │ │ ├── diff-kustomization/ │ │ │ ├── deployment.yaml │ │ │ ├── diff-with-deployment.golden │ │ │ ├── diff-with-dockerconfigjson-sops-secret.golden │ │ │ ├── diff-with-drifted-key-sops-secret.golden │ │ │ ├── diff-with-drifted-secret.golden │ │ │ ├── diff-with-drifted-service.golden │ │ │ ├── diff-with-drifted-stringdata-sops-secret.golden │ │ │ ├── diff-with-drifted-value-sops-secret.golden │ │ │ ├── diff-with-recursive.golden │ │ │ ├── dockerconfigjson-sops-secret.yaml │ │ │ ├── flux-kustomization-multiobj.yaml │ │ │ ├── key-sops-secret.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── my-app.yaml │ │ │ ├── nothing-is-deployed.golden │ │ │ ├── secret.yaml │ │ │ ├── service.yaml │ │ │ ├── stringdata-sops-secret.yaml │ │ │ └── value-sops-secret.yaml │ │ ├── envsubst/ │ │ │ ├── file.gold │ │ │ └── file.yaml │ │ ├── export/ │ │ │ ├── alert.yaml │ │ │ ├── bucket.yaml │ │ │ ├── external-artifact.yaml │ │ │ ├── git-repo.yaml │ │ │ ├── helm-chart.yaml │ │ │ ├── helm-release.yaml │ │ │ ├── helm-repo.yaml │ │ │ ├── image-policy.yaml │ │ │ ├── image-repo.yaml │ │ │ ├── image-update.yaml │ │ │ ├── ks.yaml │ │ │ ├── objects.yaml │ │ │ ├── provider.yaml │ │ │ └── receiver.yaml │ │ ├── get/ │ │ │ ├── get.golden │ │ │ ├── get_label_one.golden │ │ │ ├── get_label_two.golden │ │ │ └── objects.yaml │ │ ├── helmrelease/ │ │ │ ├── create_helmrelease_from_git.golden │ │ │ ├── create_source_git.golden │ │ │ ├── delete_helmrelease_from_git.golden │ │ │ ├── get_helmrelease_from_git.golden │ │ │ ├── reconcile_helmrelease_from_git.golden │ │ │ ├── resume_helmrelease_from_git.golden │ │ │ └── suspend_helmrelease_from_git.golden │ │ ├── image/ │ │ │ ├── create_image_policy.golden │ │ │ ├── create_image_repository.golden │ │ │ ├── get_image_policy_regex.golden │ │ │ ├── get_image_policy_semver.golden │ │ │ ├── reconcile_image_policy.golden │ │ │ ├── resume_image_policy.golden │ │ │ └── suspend_image_policy.golden │ │ ├── kustomization/ │ │ │ ├── create_kustomization_from_git.golden │ │ │ ├── create_source_git.golden │ │ │ ├── delete_kustomization_from_git.golden │ │ │ ├── get_kustomization_from_git.golden │ │ │ ├── reconcile_kustomization_from_git.golden │ │ │ ├── resume_kustomization_from_git.golden │ │ │ ├── resume_kustomization_from_git_multiple_args.golden │ │ │ ├── resume_kustomization_from_git_multiple_args_wait.golden │ │ │ ├── suspend_kustomization_from_git.golden │ │ │ └── suspend_kustomization_from_git_multiple_args.golden │ │ ├── logs/ │ │ │ ├── all-logs.txt │ │ │ ├── kind.txt │ │ │ ├── log-level.txt │ │ │ ├── multiple-filters.txt │ │ │ └── namespace.txt │ │ ├── migrate/ │ │ │ └── file-system/ │ │ │ ├── dir/ │ │ │ │ ├── some-dir/ │ │ │ │ │ ├── another-file │ │ │ │ │ ├── another-file.yaml │ │ │ │ │ └── another-file.yml │ │ │ │ ├── some-file │ │ │ │ ├── some-file.yaml │ │ │ │ └── some-file.yml │ │ │ ├── dir.golden/ │ │ │ │ ├── some-dir/ │ │ │ │ │ ├── another-file │ │ │ │ │ ├── another-file.yaml │ │ │ │ │ └── another-file.yml │ │ │ │ ├── some-file │ │ │ │ ├── some-file.yaml │ │ │ │ └── some-file.yml │ │ │ ├── dir.output.golden │ │ │ ├── single-file-wrong-ext.json │ │ │ ├── single-file.yaml │ │ │ ├── single-file.yaml.golden │ │ │ └── single-file.yaml.output.golden │ │ ├── oci/ │ │ │ ├── create_source_oci.golden │ │ │ ├── delete_oci.golden │ │ │ ├── export.golden │ │ │ ├── export_with_complete_verification.golden │ │ │ ├── export_with_issuer.golden │ │ │ ├── export_with_secret.golden │ │ │ ├── export_with_subject.golden │ │ │ ├── export_with_verify_secret.golden │ │ │ ├── get_oci.golden │ │ │ ├── reconcile_oci.golden │ │ │ ├── resume_oci.golden │ │ │ └── suspend_oci.golden │ │ ├── trace/ │ │ │ ├── deployment-hr-ocirepo.golden │ │ │ ├── deployment-hr-ocirepo.yaml │ │ │ ├── deployment.golden │ │ │ ├── deployment.yaml │ │ │ ├── helmrelease-oci.golden │ │ │ ├── helmrelease-oci.yaml │ │ │ ├── helmrelease.golden │ │ │ └── helmrelease.yaml │ │ └── tree/ │ │ ├── kustomizations.yaml │ │ ├── tree-compact.golden │ │ ├── tree-empty.golden │ │ └── tree.golden │ ├── trace.go │ ├── trace_test.go │ ├── tree.go │ ├── tree_artifact.go │ ├── tree_artifact_generator.go │ ├── tree_kustomization.go │ ├── tree_kustomization_test.go │ ├── uninstall.go │ ├── version.go │ ├── version_test.go │ ├── version_utils.go │ └── version_utils_test.go ├── docs/ │ └── release/ │ ├── README.md │ └── release-notes-template.md ├── go.mod ├── go.sum ├── install/ │ ├── README.md │ └── flux.sh ├── internal/ │ ├── build/ │ │ ├── build.go │ │ ├── build_test.go │ │ ├── diff.go │ │ └── testdata/ │ │ └── local-kustomization/ │ │ ├── different-name.yaml │ │ ├── invalid-resource.yaml │ │ ├── multi-doc-reset.yaml │ │ ├── multi-doc-valid.yaml │ │ ├── no-ns.yaml │ │ └── valid.yaml │ ├── flags/ │ │ ├── crds.go │ │ ├── crds_test.go │ │ ├── decryption_provider.go │ │ ├── decryption_provider_test.go │ │ ├── ecdsa_curve.go │ │ ├── ecdsa_curve_test.go │ │ ├── gitlab_visibility.go │ │ ├── gitlab_visibility_test.go │ │ ├── helm_chart_source.go │ │ ├── helm_chart_source_test.go │ │ ├── kustomization_source.go │ │ ├── kustomization_source_test.go │ │ ├── local_helm_chart_source.go │ │ ├── log_level.go │ │ ├── log_level_test.go │ │ ├── public_key_algorithm.go │ │ ├── public_key_algorithm_test.go │ │ ├── rsa_key_bits.go │ │ ├── rsa_key_bits_test.go │ │ ├── safe_relative_path.go │ │ ├── safe_relative_path_test.go │ │ ├── source_bucket_provider.go │ │ ├── source_bucket_provider_test.go │ │ ├── source_git_provider.go │ │ ├── source_oci_provider.go │ │ ├── source_oci_verify_provider.go │ │ └── source_oci_verify_provider_test.go │ ├── tree/ │ │ └── tree.go │ └── utils/ │ ├── apply.go │ ├── hex.go │ ├── hex_test.go │ ├── testdata/ │ │ ├── components-with-crds.yaml │ │ └── components-without-crds.yaml │ ├── utils.go │ └── utils_test.go ├── manifests/ │ ├── bases/ │ │ ├── helm-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ ├── image-automation-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ ├── image-reflector-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ ├── kustomize-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ ├── notification-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ ├── source-controller/ │ │ │ ├── account.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── labels.yaml │ │ │ └── patch.yaml │ │ └── source-watcher/ │ │ ├── account.yaml │ │ ├── kustomization.yaml │ │ ├── labels.yaml │ │ └── patch.yaml │ ├── crds/ │ │ └── kustomization.yaml │ ├── install/ │ │ ├── kustomization.yaml │ │ ├── labels.yaml │ │ └── namespace.yaml │ ├── monitoring/ │ │ └── README.md │ ├── openshift/ │ │ ├── kustomization.yaml │ │ ├── labels.yaml │ │ ├── namespace.yaml │ │ └── scc.yaml │ ├── policies/ │ │ ├── allow-egress.yaml │ │ ├── allow-scraping.yaml │ │ ├── allow-webhooks.yaml │ │ └── kustomization.yaml │ ├── rbac/ │ │ ├── controller.yaml │ │ ├── edit.yaml │ │ ├── kustomization.yaml │ │ ├── reconciler.yaml │ │ ├── resourcequota.yaml │ │ └── view.yaml │ ├── scripts/ │ │ └── bundle.sh │ └── test/ │ ├── kustomization.yaml │ ├── labels.yaml │ └── namespace.yaml ├── netlify.toml ├── pkg/ │ ├── bootstrap/ │ │ ├── bootstrap.go │ │ ├── bootstrap_plain_git.go │ │ ├── bootstrap_provider.go │ │ ├── bootstrap_test.go │ │ ├── options.go │ │ └── provider/ │ │ ├── factory.go │ │ └── provider.go │ ├── log/ │ │ ├── log.go │ │ └── nop.go │ ├── manifestgen/ │ │ ├── doc.go │ │ ├── install/ │ │ │ ├── install.go │ │ │ ├── install_test.go │ │ │ ├── manifests.go │ │ │ ├── options.go │ │ │ └── templates.go │ │ ├── kustomization/ │ │ │ ├── kustomization.go │ │ │ └── options.go │ │ ├── labels.go │ │ ├── manifest.go │ │ ├── sourcesecret/ │ │ │ ├── options.go │ │ │ ├── sourcesecret.go │ │ │ ├── sourcesecret_test.go │ │ │ └── testdata/ │ │ │ ├── password_rsa │ │ │ ├── password_rsa.pub │ │ │ ├── rsa │ │ │ └── rsa.pub │ │ ├── sync/ │ │ │ ├── options.go │ │ │ ├── sync.go │ │ │ └── sync_test.go │ │ └── tmpdir.go │ ├── printers/ │ │ ├── dyff.go │ │ ├── interface.go │ │ └── table_printer.go │ ├── status/ │ │ └── status.go │ └── uninstall/ │ └── uninstall.go ├── rfcs/ │ ├── 0001-authorization/ │ │ └── README.md │ ├── 0002-helm-oci/ │ │ └── README.md │ ├── 0003-kubernetes-oci/ │ │ └── README.md │ ├── 0004-insecure-http/ │ │ └── README.md │ ├── 0005-artifact-revision-and-digest/ │ │ └── README.md │ ├── 0006-cdevents/ │ │ └── README.md │ ├── 0007-git-repo-passwordless-auth/ │ │ └── README.md │ ├── 0008-custom-event-metadata-from-annotations/ │ │ └── README.md │ ├── 0009-custom-health-checks/ │ │ └── README.md │ ├── 0010-multi-tenant-workload-identity/ │ │ └── README.md │ ├── 0011-opentelemetry-tracing/ │ │ └── README.md │ ├── 0012-external-artifact/ │ │ └── README.md │ ├── README.md │ └── RFC-0000/ │ └── README.md └── tests/ ├── .gitignore ├── bootstrap/ │ └── main.go ├── image-automation/ │ ├── auto.yaml │ ├── kustomization.yaml │ └── main.go └── integration/ ├── Makefile ├── README.md ├── azure_specific_test.go ├── azure_test.go ├── flux_test.go ├── gcp_test.go ├── go.mod ├── go.sum ├── image_repo_test.go ├── notification_test.go ├── oci_test.go ├── sops_encryption_test.go ├── suite_test.go ├── terraform/ │ ├── azure/ │ │ ├── aks.tf │ │ ├── azuredevops.tf │ │ ├── event-hub.tf │ │ ├── keyvault.tf │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── gcp/ │ ├── gke.tf │ ├── kms.tf │ ├── main.tf │ ├── outputs.tf │ ├── pubsub.tf │ ├── sourcerepo.tf │ └── variables.tf └── util_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ --- name: Bug report description: Create a report to help us improve Flux body: - type: markdown attributes: value: | ## Support Find out more about your support options and getting help at: https://fluxcd.io/support/ - type: textarea validations: required: true attributes: label: Describe the bug description: A clear description of what the bug is. - type: textarea validations: required: true attributes: label: Steps to reproduce description: | Steps to reproduce the problem. placeholder: | For example: 1. Install Flux with the additional image automation controllers 2. Run command '...' 3. See error - type: textarea validations: required: true attributes: label: Expected behavior description: A brief description of what you expected to happen. - type: textarea attributes: label: Screenshots and recordings description: | If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/ - type: input validations: required: true attributes: label: OS / Distro description: The OS / distro you are executing `flux` on. If not applicable, write `N/A`. placeholder: e.g. Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15... - type: input validations: required: true attributes: label: Flux version description: Run `flux version --client`. If not applicable, write `N/A`. placeholder: e.g. v0.20.1 - type: textarea validations: required: true attributes: label: Flux check description: Run `flux check`. If not applicable, write `N/A`. placeholder: | For example: ► checking prerequisites ✔ Kubernetes 1.21.1 >=1.19.0-0 ► checking controllers ✔ all checks passed - type: input attributes: label: Git provider description: If applicable, add the Git provider you are having problems with, e.g. GitHub (Enterprise), GitLab, etc. - type: input attributes: label: Container Registry provider description: If applicable, add the Container Registry provider you are having problems with, e.g. DockerHub, GitHub Packages, Quay.io, etc. - type: textarea attributes: label: Additional context description: Add any other context about the problem here. This can be logs (e.g. output from `flux logs`), environment specific caveats, etc. - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fluxcd/.github/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/fluxcd/flux2/discussions about: Please ask and answer questions here. ================================================ FILE: .github/aur/flux-bin/.SRCINFO.template ================================================ pkgbase = flux-bin pkgdesc = Open and extensible continuous delivery solution for Kubernetes pkgver = ${PKGVER} pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 arch = armv7h arch = aarch64 license = APACHE optdepends = bash-completion: auto-completion for flux in Bash optdepends = zsh-completions: auto-completion for flux in ZSH source_x86_64 = flux-bin-${PKGVER}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz sha256sums_x86_64 = ${SHA256SUM_AMD64} source_armv7h = flux-bin-${PKGVER}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz sha256sums_armv7h = ${SHA256SUM_ARM} source_aarch64 = flux-bin-${PKGVER}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm64.tar.gz sha256sums_aarch64 = ${SHA256SUM_ARM64} pkgname = flux-bin ================================================ FILE: .github/aur/flux-bin/.gitignore ================================================ .pkg ================================================ FILE: .github/aur/flux-bin/PKGBUILD.template ================================================ # Maintainer: Aurel Canciu # Maintainer: Hidde Beydals pkgname=flux-bin pkgver=${PKGVER} pkgrel=${PKGREL} _srcname=flux _srcver=${VERSION} pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" arch=("x86_64" "armv7h" "aarch64") license=("APACHE") optdepends=('bash-completion: auto-completion for flux in Bash' 'zsh-completions: auto-completion for flux in ZSH') source_x86_64=( "${pkgname}-${pkgver}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_amd64.tar.gz" ) source_armv7h=( "${pkgname}-${pkgver}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz" ) source_aarch64=( "${pkgname}-${pkgver}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm64.tar.gz" ) sha256sums_x86_64=( ${SHA256SUM_AMD64} ) sha256sums_armv7h=( ${SHA256SUM_ARM} ) sha256sums_aarch64=( ${SHA256SUM_ARM64} ) package() { install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" "${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}" "${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish" "${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}" } ================================================ FILE: .github/aur/flux-bin/publish.sh ================================================ #!/usr/bin/env bash set -e WD=$(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd) PKGNAME=$(basename $WD) ROOT=${WD%/.github/aur/$PKGNAME} LOCKFILE=/tmp/aur-$PKGNAME.lock exec 100>$LOCKFILE || exit 0 flock -n 100 || exit 0 trap "rm -f $LOCKFILE" EXIT export VERSION=$1 echo "Publishing to AUR as version ${VERSION}" cd $WD export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" eval $(ssh-agent -s) ssh-add <(echo "$AUR_BOT_SSH_PRIVATE_KEY") GITDIR=$(mktemp -d /tmp/aur-$PKGNAME-XXX) trap "rm -rf $GITDIR" EXIT git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') # Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then export PKGREL=$((CURRENT_PKGREL+1)) else export PKGREL=1 fi export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm.tar.gz | awk '{ print $1 }') export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm64.tar.gz | awk '{ print $1 }') export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_amd64.tar.gz | awk '{ print $1 }') envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < PKGBUILD.template > $GITDIR/PKGBUILD cd $GITDIR git config user.name "fluxcdbot" git config user.email "fluxcdbot@users.noreply.github.com" git add -A if [ -z "$(git status --porcelain)" ]; then echo "No changes." else git commit -m "Updated to version v${PKGVER} release ${PKGREL}" git push origin master fi ================================================ FILE: .github/aur/flux-go/.SRCINFO.template ================================================ pkgbase = flux-go pkgdesc = Open and extensible continuous delivery solution for Kubernetes pkgver = ${PKGVER} pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 arch = armv7h arch = aarch64 license = APACHE makedepends = go depends = glibc provides = flux-bin conflicts = flux-bin replaces = flux-cli source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz pkgname = flux-go ================================================ FILE: .github/aur/flux-go/.gitignore ================================================ .pkg ================================================ FILE: .github/aur/flux-go/PKGBUILD.template ================================================ # Maintainer: Aurel Canciu # Maintainer: Hidde Beydals pkgname=flux-go pkgver=${PKGVER} pkgrel=${PKGREL} _srcname=flux _srcver=${VERSION} pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" arch=("x86_64" "armv7h" "aarch64") license=("APACHE") provides=("flux-bin") conflicts=("flux-bin") replaces=("flux-cli") depends=("glibc") makedepends=('go>=1.20', 'kustomize>=5.0') optdepends=('bash-completion: auto-completion for flux in Bash', 'zsh-completions: auto-completion for flux in ZSH') source=( "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${_srcver}.tar.gz" ) sha256sums=( ${SHA256SUM} ) build() { cd "flux2-${_srcver}" export CGO_LDFLAGS="$LDFLAGS" export CGO_CFLAGS="$CFLAGS" export CGO_CXXFLAGS="$CXXFLAGS" export CGO_CPPFLAGS="$CPPFLAGS" export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" make cmd/flux/.manifests.done go build -ldflags "-linkmode=external -X main.VERSION=${_srcver}" -o ${_srcname} ./cmd/flux } check() { cd "flux2-${_srcver}" case $CARCH in aarch64) export ENVTEST_ARCH=arm64 ;; armv7h) export ENVTEST_ARCH=arm ;; esac make test } package() { cd "flux2-${_srcver}" install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" "${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}" "${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish" "${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}" } ================================================ FILE: .github/aur/flux-go/publish.sh ================================================ #!/usr/bin/env bash set -e WD=$(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd) PKGNAME=$(basename $WD) ROOT=${WD%/.github/aur/$PKGNAME} LOCKFILE=/tmp/aur-$PKGNAME.lock exec 100>$LOCKFILE || exit 0 flock -n 100 || exit 0 trap "rm -f $LOCKFILE" EXIT export VERSION=$1 echo "Publishing to AUR as version ${VERSION}" cd $WD export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" eval $(ssh-agent -s) ssh-add <(echo "$AUR_BOT_SSH_PRIVATE_KEY") GITDIR=$(mktemp -d /tmp/aur-$PKGNAME-XXX) trap "rm -rf $GITDIR" EXIT git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') # Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then export PKGREL=$((CURRENT_PKGREL+1)) else export PKGREL=1 fi export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz | sha256sum | awk '{ print $1 }') envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD cd $GITDIR git config user.name "fluxcdbot" git config user.email "fluxcdbot@users.noreply.github.com" git add -A if [ -z "$(git status --porcelain)" ]; then echo "No changes." else git commit -m "Updated to version v${PKGVER} release ${PKGREL}" git push origin master fi ================================================ FILE: .github/aur/flux-scm/.SRCINFO.template ================================================ pkgbase = flux-scm pkgdesc = Open and extensible continuous delivery solution for Kubernetes pkgver = ${PKGVER} pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 arch = armv7h arch = aarch64 license = APACHE makedepends = go depends = glibc provides = flux-bin conflicts = flux-bin source = git+https://github.com/fluxcd/flux2.git md5sums = SKIP pkgname = flux-scm ================================================ FILE: .github/aur/flux-scm/.gitignore ================================================ .pkg ================================================ FILE: .github/aur/flux-scm/PKGBUILD.template ================================================ # Maintainer: Aurel Canciu # Maintainer: Hidde Beydals pkgname=flux-scm pkgver=${PKGVER} pkgrel=${PKGREL} _srcname=flux pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" arch=("x86_64" "armv7h" "aarch64") license=("APACHE") provides=("flux-bin") conflicts=("flux-bin") depends=("glibc") makedepends=('go>=1.20', 'kustomize>=5.0', 'git') optdepends=('bash-completion: auto-completion for flux in Bash', 'zsh-completions: auto-completion for flux in ZSH') source=( "git+https://github.com/fluxcd/flux2.git" ) md5sums=('SKIP') pkgver() { cd "flux2" printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" } build() { cd "flux2" export CGO_LDFLAGS="$LDFLAGS" export CGO_CFLAGS="$CFLAGS" export CGO_CXXFLAGS="$CXXFLAGS" export CGO_CPPFLAGS="$CPPFLAGS" export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" make cmd/flux/.manifests.done go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux } check() { cd "flux2" case $CARCH in aarch64) export ENVTEST_ARCH=arm64 ;; armv7h) export ENVTEST_ARCH=arm ;; esac make test } package() { cd "flux2" install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" "${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}" "${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish" "${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}" } ================================================ FILE: .github/aur/flux-scm/publish.sh ================================================ #!/usr/bin/env bash set -e WD=$(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd) PKGNAME=$(basename $WD) ROOT=${WD%/.github/aur/$PKGNAME} LOCKFILE=/tmp/aur-$PKGNAME.lock exec 100>$LOCKFILE || exit 0 flock -n 100 || exit 0 trap "rm -f $LOCKFILE" EXIT export VERSION=$1 echo "Publishing to AUR as version ${VERSION}" cd $WD export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" eval $(ssh-agent -s) ssh-add <(echo "$AUR_BOT_SSH_PRIVATE_KEY") GITDIR=$(mktemp -d /tmp/aur-$PKGNAME-XXX) trap "rm -rf $GITDIR" EXIT git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') # Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then export PKGREL=$((CURRENT_PKGREL+1)) else export PKGREL=1 fi envsubst '$PKGVER $PKGREL' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$PKGVER $PKGREL' < PKGBUILD.template > $GITDIR/PKGBUILD cd $GITDIR git config user.name "fluxcdbot" git config user.email "fluxcdbot@users.noreply.github.com" git add -A if [ -z "$(git status --porcelain)" ]; then echo "No changes." else git commit -m "Updated to version v${PKGVER} release ${PKGREL}" git push origin master fi ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" labels: ["area/ci", "dependencies"] groups: # Group all updates together, so that they are all applied in a single PR. # xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups ci: patterns: - "*" schedule: interval: "monthly" ================================================ FILE: .github/kind/config.yaml ================================================ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker networking: disableDefaultCNI: true # disable kindnet podSubnet: 192.168.0.0/16 # set to Calico's default subnet ================================================ FILE: .github/labels.yaml ================================================ # Configuration file to declaratively configure labels # Ref: https://github.com/EndBug/label-sync#Config-files - name: area/bootstrap description: Bootstrap related issues and pull requests color: '#86efc9' - name: area/install description: Install and uninstall related issues and pull requests color: '#86efc9' - name: area/diff description: Diff related issues and pull requests color: '#BA4192' - name: area/bucket description: Bucket related issues and pull requests color: '#00b140' - name: area/git description: Git related issues and pull requests color: '#863faf' - name: area/oci description: OCI related issues and pull requests color: '#c739ff' - name: area/kustomization description: Kustomization related issues and pull requests color: '#00e54d' - name: area/helm description: Helm related issues and pull requests color: '#1673b6' - name: area/image-automation description: Automated image updates related issues and pull requests color: '#c5def5' - name: area/monitoring description: Monitoring related issues and pull requests color: '#dd75ae' - name: area/multi-tenancy description: Multi-tenancy related issues and pull requests color: '#72CDBD' - name: area/notification description: Notification API related issues and pull requests color: '#434ec1' - name: area/source description: Source API related issues and pull requests color: '#863faf' - name: area/rfc description: Feature request proposals in the RFC format color: '#D621C3' aliases: ['area/RFC'] - name: backport:release/v2.6.x description: To be backported to release/v2.6.x color: '#ffd700' - name: backport:release/v2.7.x description: To be backported to release/v2.7.x color: '#ffd700' - name: backport:release/v2.8.x description: To be backported to release/v2.8.x color: '#ffd700' ================================================ FILE: .github/runners/README.md ================================================ # Flux ARM64 GitHub runners The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners. ## Current instances | Repository | Runner | Instance | Location | |-----------------------------|------------------|----------------|---------------| | flux2 | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC | | flux2 | equinix-arm-dc-2 | flux-arm-dc-01 | Washington DC | | flux2 | equinix-arm-da-1 | flux-arm-da-01 | Dallas | | flux2 | equinix-arm-da-2 | flux-arm-da-01 | Dallas | | flux-benchmark | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC | | flux-benchmark | equinix-arm-da-1 | flux-arm-da-01 | Dallas | | source-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC | | source-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas | | image-automation-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC | | image-automation-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas | Instance spec: - Ampere Altra Q80-30 80-core processor @ 2.8GHz - 2 x 960GB NVME - 256GB RAM - 2 x 25Gbps ## Instance setup In order to add a new runner to the GitHub Actions pool, first create a server on Equinix with the following configuration: - Type: `c3.large.arm64` - OS: `Ubuntu 22.04 LTS` ### Install prerequisites - SSH into a newly created instance ```shell ssh root@ ``` - Create the ubuntu user ```shell adduser ubuntu usermod -aG sudo ubuntu su - ubuntu ``` - Create the prerequisites dir ```shell mkdir -p prereq && cd prereq ``` - Download the prerequisites script ```shell curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/prereq.sh > prereq.sh \ && chmod +x ./prereq.sh ``` - Install the prerequisites ```shell sudo ./prereq.sh ``` ### Install runners - Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux) - Create two directories `flux2-01`, `flux2-02` - In each dir run: ```shell curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/runner-setup.sh > runner-setup.sh \ && chmod +x ./runner-setup.sh ./runner-setup.sh equinix-arm- ``` - Reboot the instance ```shell sudo reboot ``` - Navigate to the GitHub repository [runners page](https://github.com/fluxcd/flux2/settings/actions/runners) and check the runner status ================================================ FILE: .github/runners/prereq.sh ================================================ #!/usr/bin/env bash # Copyright 2021 The Flux authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This script installs the prerequisites for running Flux end-to-end tests with Docker and GitHub self-hosted runners. set -eu KIND_VERSION=0.22.0 KUBECTL_VERSION=1.29.0 KUSTOMIZE_VERSION=5.3.0 HELM_VERSION=3.14.1 GITHUB_RUNNER_VERSION=2.313.0 PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq pkg-config" # install prerequisites apt-get update \ && apt-get install -y -q ${PACKAGES} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # fix Kubernetes DNS resolution rm /etc/resolv.conf cat "/run/systemd/resolve/stub-resolv.conf" | sed '/search/d' > /etc/resolv.conf # install docker curl -fsSL https://get.docker.com -o get-docker.sh \ && chmod +x get-docker.sh ./get-docker.sh systemctl enable docker.service systemctl enable containerd.service usermod -aG docker ubuntu # install kind curl -Lo ./kind https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-arm64 install -o root -g root -m 0755 kind /usr/local/bin/kind # install kubectl curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/arm64/kubectl" install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl # install kustomize curl -Lo ./kustomize.tar.gz https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_arm64.tar.gz \ && tar -zxvf kustomize.tar.gz \ && rm kustomize.tar.gz install -o root -g root -m 0755 kustomize /usr/local/bin/kustomize # install helm curl -Lo ./helm.tar.gz https://get.helm.sh/helm-v${HELM_VERSION}-linux-arm64.tar.gz \ && tar -zxvf helm.tar.gz \ && rm helm.tar.gz install -o root -g root -m 0755 linux-arm64/helm /usr/local/bin/helm # download runner curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \ && tar xzf actions-runner-linux-arm64.tar.gz \ && rm actions-runner-linux-arm64.tar.gz # install runner dependencies ./bin/installdependencies.sh ================================================ FILE: .github/runners/runner-setup.sh ================================================ #!/usr/bin/env bash # Copyright 2021 The Flux authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This script installs a GitHub self-hosted ARM64 runner for running Flux end-to-end tests. set -eu RUNNER_NAME=$1 REPOSITORY_TOKEN=$2 REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2} GITHUB_RUNNER_VERSION=2.313.0 # download runner curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \ && tar xzf actions-runner-linux-arm64.tar.gz \ && rm actions-runner-linux-arm64.tar.gz # register runner with GitHub ./config.sh --unattended --url ${REPOSITORY_URL} --token ${REPOSITORY_TOKEN} --name ${RUNNER_NAME} # start runner sudo ./svc.sh install sudo ./svc.sh start ================================================ FILE: .github/workflows/README.md ================================================ # Flux GitHub Workflows ## End-to-end Testing The e2e workflows run a series of tests to ensure that the Flux CLI and the GitOps Toolkit controllers work well all together. The tests are written in Go, Bash, Make and Terraform. | Workflow | Jobs | Runner | Role | |--------------------|----------------------|----------------|-----------------------------------------------| | e2e.yaml | e2e-amd64-kubernetes | GitHub Ubuntu | integration testing with Kubernetes Kind
| | e2e-arm64.yaml | e2e-arm64-kubernetes | Equinix Ubuntu | integration testing with Kubernetes Kind
| | e2e-bootstrap.yaml | e2e-boostrap-github | GitHub Ubuntu | integration testing with GitHub API
| | e2e-azure.yaml | e2e-amd64-aks | GitHub Ubuntu | integration testing with Azure API
| | scan.yaml | scan-fossa | GitHub Ubuntu | license scanning
| | scan.yaml | scan-snyk | GitHub Ubuntu | vulnerability scanning
| | scan.yaml | scan-codeql | GitHub Ubuntu | vulnerability scanning
| ## Components Update The components update workflow scans the GitOps Toolkit controller repositories for new releases, amd when it finds a new controller version, the workflow performs the following steps: - Updates the controller API package version in `go.mod`. - Patches the controller CRDs version in the `manifests/crds` overlay. - Patches the controller Deployment version in `manifests/bases` overlay. - Opens a Pull Request against the checked out branch. - Triggers the e2e test suite to run for the opened PR. | Workflow | Jobs | Runner | Role | |-------------|-------------------|---------------|-----------------------------------------------------| | update.yaml | update-components | GitHub Ubuntu | update the GitOps Toolkit APIs and controllers
| ## Release The release workflow is triggered by a semver Git tag and performs the following steps: - Generates the Flux install manifests (YAML). - Generates the OpenAPI validation schemas for the GitOps Toolkit CRDs (JSON). - Generates a Software Bill of Materials (SPDX JSON). - Builds the Flux CLI binaries and the multi-arch container images. - Pushes the container images to GitHub Container Registry and DockerHub. - Signs the sbom, the binaries checksum and the container images with Cosign and GitHub OIDC. - Uploads the sbom, binaries, checksums and install manifests to GitHub Releases. - Pushes the install manifests as OCI artifacts to GitHub Container Registry and DockerHub. - Signs the OCI artifacts with Cosign and GitHub OIDC. | Workflow | Jobs | Runner | Role | |--------------|------------------------|---------------|------------------------------------------------------| | release.yaml | release-flux-cli | GitHub Ubuntu | build, push and sign the CLI release artifacts
| | release.yaml | release-flux-manifests | GitHub Ubuntu | build, push and sign the Flux install manifests
| ================================================ FILE: .github/workflows/action.yaml ================================================ name: test-gh-action on: pull_request: paths: - 'action/**' push: paths: - 'action/**' branches: - 'main' - 'release/**' permissions: read-all jobs: actions: strategy: fail-fast: false matrix: version: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.version }} name: action on ${{ matrix.version }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup flux uses: ./action ================================================ FILE: .github/workflows/backport.yaml ================================================ name: backport on: pull_request_target: types: [closed, labeled] permissions: read-all jobs: backport: permissions: contents: write # for reading and creating branches. pull-requests: write # for creating pull requests against release branches. uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0 secrets: github-token: ${{ secrets.BOT_GITHUB_TOKEN }} ================================================ FILE: .github/workflows/conformance.yaml ================================================ name: conformance on: workflow_dispatch: push: branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ] permissions: contents: read env: GO_VERSION: 1.26.x jobs: conform-kubernetes: runs-on: group: "ARM64" strategy: matrix: # Keep this list up-to-date with https://endoflife.date/kubernetes # Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0] fail-fast: false steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: | **/go.sum **/go.mod - name: Prepare id: prep run: | ID=${GITHUB_SHA:0:7}-${{ matrix.KUBERNETES_VERSION }}-$(date +%s) echo "CLUSTER=arm64-${ID}" >> $GITHUB_OUTPUT - name: Build run: | make build - name: Setup Kubernetes uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: version: v0.30.0 cluster_name: ${{ steps.prep.outputs.CLUSTER }} node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64 - name: Run e2e tests run: TEST_KUBECONFIG=$HOME/.kube/config make e2e - name: Run multi-tenancy tests run: | ./bin/flux install ./bin/flux create source git flux-system \ --interval=15m \ --url=https://github.com/fluxcd/flux2-multi-tenancy \ --branch=main \ --ignore-paths="./clusters/**/flux-system/" ./bin/flux create kustomization flux-system \ --interval=15m \ --source=flux-system \ --path=./clusters/staging kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m - name: Debug failure if: failure() run: | kubectl -n flux-system get all kubectl -n flux-system describe po kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/kustomize-controller conform-k3s: runs-on: ubuntu-latest strategy: matrix: # Keep this list up-to-date with https://endoflife.date/kubernetes # Available versions can be found with "replicated cluster versions" K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ] fail-fast: false steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: | **/go.sum **/go.mod - name: Prepare id: prep run: | ID=${GITHUB_SHA:0:7}-${{ matrix.K3S_VERSION }}-$(date +%s) PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}') echo "cluster=flux2-k3s-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Build run: make build-dev - name: Create repository run: | gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }} env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Create cluster id: create-cluster uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 with: api-token: ${{ secrets.REPLICATED_API_TOKEN }} kubernetes-distribution: "k3s" kubernetes-version: ${{ matrix.K3S_VERSION }} ttl: 20m cluster-name: "${{ steps.prep.outputs.cluster }}" kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }} export-kubeconfig: true - name: Run e2e tests run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e - name: Run flux bootstrap run: | ./bin/flux bootstrap git --manifests ./manifests/test/ \ --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ --branch=main \ --path=clusters/k3s \ --token-auth env: GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Run flux check run: | ./bin/flux check - name: Run flux reconcile run: | ./bin/flux reconcile ks flux-system --with-source ./bin/flux get all ./bin/flux events - name: Collect reconcile logs if: ${{ always() }} continue-on-error: true run: | kubectl -n flux-system get all kubectl -n flux-system describe pods kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/kustomize-controller kubectl -n flux-system logs deploy/notification-controller - name: Delete flux run: | ./bin/flux uninstall -s --keep-namespace kubectl delete ns flux-system --wait - name: Delete cluster if: ${{ always() }} uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 continue-on-error: true with: api-token: ${{ secrets.REPLICATED_API_TOKEN }} cluster-id: ${{ steps.create-cluster.outputs.cluster-id }} - name: Delete repository if: ${{ always() }} continue-on-error: true run: | gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} conform-openshift: runs-on: ubuntu-latest strategy: matrix: # Keep this list up-to-date with https://endoflife.date/red-hat-openshift OPENSHIFT_VERSION: [ 4.20.0-okd ] fail-fast: false steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: | **/go.sum **/go.mod - name: Prepare id: prep run: | ID=${GITHUB_SHA:0:7}-${{ matrix.OPENSHIFT_VERSION }}-$(date +%s) PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}') echo "cluster=flux2-openshift-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Build run: make build-dev - name: Create repository run: | gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }} env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Create cluster id: create-cluster uses: replicatedhq/replicated-actions/create-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 with: api-token: ${{ secrets.REPLICATED_API_TOKEN }} kubernetes-distribution: "openshift" kubernetes-version: ${{ matrix.OPENSHIFT_VERSION }} ttl: 20m cluster-name: "${{ steps.prep.outputs.cluster }}" kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }} export-kubeconfig: true - name: Run flux bootstrap run: | ./bin/flux bootstrap git --manifests ./manifests/openshift/ \ --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ --branch=main \ --path=clusters/openshift \ --token-auth env: GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Run flux check run: | ./bin/flux check - name: Run flux reconcile run: | ./bin/flux reconcile ks flux-system --with-source ./bin/flux get all ./bin/flux events - name: Collect reconcile logs if: ${{ always() }} continue-on-error: true run: | kubectl -n flux-system get all kubectl -n flux-system describe pods kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/kustomize-controller kubectl -n flux-system logs deploy/notification-controller - name: Delete flux run: | ./bin/flux uninstall -s --keep-namespace kubectl delete ns flux-system --wait - name: Delete cluster if: ${{ always() }} uses: replicatedhq/replicated-actions/remove-cluster@1abb33f5274580b14f49f2a12d819df7920e4d9b # v1.20.0 continue-on-error: true with: api-token: ${{ secrets.REPLICATED_API_TOKEN }} cluster-id: ${{ steps.create-cluster.outputs.cluster-id }} - name: Delete repository if: ${{ always() }} continue-on-error: true run: | gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} ================================================ FILE: .github/workflows/e2e-azure.yaml ================================================ name: e2e-azure on: workflow_dispatch: schedule: - cron: '0 6 * * *' push: branches: - main paths: - 'tests/**' - '.github/workflows/e2e-azure.yaml' pull_request: branches: - main paths: - 'tests/**' - '.github/workflows/e2e-azure.yaml' permissions: contents: read jobs: e2e-aks: runs-on: ubuntu-latest defaults: run: working-directory: ./tests/integration if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - name: CheckoutD uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: tests/integration/go.sum - name: Setup Terraform uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 - name: Setup Flux CLI run: make build working-directory: ./ - name: Setup SOPS run: | mkdir -p $HOME/.local/bin wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux chmod +x $HOME/.local/bin/sops env: SOPS_VER: 3.7.1 - name: Authenticate to Azure uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6 with: creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}' - name: Set dynamic variables in .env run: | cat > .env < build/ssh/key $GITREPO_SSH_CONTENTS EOF export GITREPO_SSH_PATH=build/ssh/key cat < build/ssh/key.pub $GITREPO_SSH_PUB_CONTENTS EOF export GITREPO_SSH_PUB_PATH=build/ssh/key.pub make test-azure - name: Ensure resource cleanup if: ${{ always() }} env: ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }} TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }} TF_VAR_azure_location: ${{ vars.TF_VAR_azure_location }} run: source .env && make destroy-azure ================================================ FILE: .github/workflows/e2e-bootstrap.yaml ================================================ name: e2e-bootstrap on: workflow_dispatch: push: branches: [ 'main', 'release/**' ] pull_request: branches: [ 'main', 'release/**' ] paths-ignore: [ 'docs/**', 'rfcs/**' ] permissions: contents: read jobs: e2e-boostrap-github: runs-on: ubuntu-latest if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: | **/go.sum **/go.mod - name: Setup Kubernetes uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: version: v0.30.0 cluster_name: kind # The versions below should target the newest Kubernetes version # Keep this up-to-date with https://endoflife.date/kubernetes node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64 kubectl_version: v1.33.0 - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Setup yq uses: fluxcd/pkg/actions/yq@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Build run: make build-dev - name: Set outputs id: vars run: | REPOSITORY_NAME=${{ github.event.repository.name }} BRANCH_NAME=${GITHUB_REF##*/} COMMIT_SHA=$(git rev-parse HEAD) PSEUDO_RAND_SUFFIX=$(echo "${BRANCH_NAME}-${COMMIT_SHA}" | shasum | awk '{print $1}') TEST_REPO_NAME="${REPOSITORY_NAME}-${PSEUDO_RAND_SUFFIX}" echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT - name: bootstrap init run: | ./bin/flux bootstrap github --manifests ./manifests/test/ \ --owner=fluxcd-testing \ --image-pull-secret=ghcr-auth \ --registry-creds=fluxcd:$GITHUB_TOKEN \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ --team=team-z env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: verify image pull secret run: | kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson - name: bootstrap no-op run: | ./bin/flux bootstrap github --manifests ./manifests/test/ \ --owner=fluxcd-testing \ --image-pull-secret=ghcr-auth \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ --team=team-z env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: bootstrap customize run: | make setup-bootstrap-patch ./bin/flux bootstrap github --manifests ./manifests/test/ \ --owner=fluxcd-testing \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ --team=team-z if [ $(kubectl get deployments.apps source-controller -o jsonpath='{.spec.template.spec.securityContext.runAsUser}') != "10000" ]; then echo "Bootstrap not customized as controller is not running as user 10000" && exit 1 fi env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }} GITHUB_ORG_NAME: fluxcd-testing - name: uninstall run: | ./bin/flux uninstall -s --keep-namespace kubectl delete ns flux-system --timeout=10m --wait=true - name: test image automation run: | make setup-image-automation ./bin/flux bootstrap github --manifests ./manifests/test/ \ --owner=fluxcd-testing \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ --read-write-key ./bin/flux reconcile image repository podinfo ./bin/flux reconcile image policy podinfo ./bin/flux reconcile image update flux-system ./bin/flux get images all ./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system | \ yq '.status.lastPushCommit | length > 1' | grep 'true' env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }} GITHUB_ORG_NAME: fluxcd-testing - name: delete repository if: ${{ always() }} continue-on-error: true run: | gh repo delete fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} --yes env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Debug failure if: failure() run: | kubectl -n flux-system get all kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/kustomize-controller ================================================ FILE: .github/workflows/e2e-gcp.yaml ================================================ name: e2e-gcp on: workflow_dispatch: schedule: - cron: '0 6 * * *' push: branches: - main paths: - 'tests/**' - '.github/workflows/e2e-gcp.yaml' pull_request: branches: - main paths: - 'tests/**' - '.github/workflows/e2e-gcp.yaml' permissions: contents: read jobs: e2e-gcp: runs-on: ubuntu-latest defaults: run: working-directory: ./tests/integration if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: tests/integration/go.sum - name: Setup Terraform uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 - name: Setup Flux CLI run: make build working-directory: ./ - name: Setup SOPS run: | mkdir -p $HOME/.local/bin wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux chmod +x $HOME/.local/bin/sops env: SOPS_VER: 3.7.1 - name: Authenticate to Google Cloud uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 id: 'auth' with: credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}' token_format: 'access_token' - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 - name: Setup QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Setup Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log into us-central1-docker.pkg.dev uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: us-central1-docker.pkg.dev username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - name: Set dynamic variables in .env run: | cat > .env < build/ssh/key export GITREPO_SSH_PATH=build/ssh/key touch ./build/ssh/key.pub echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub export GITREPO_SSH_PUB_PATH=build/ssh/key.pub make test-gcp - name: Ensure resource cleanup if: ${{ always() }} env: TF_VAR_gcp_project_id: ${{ vars.TF_VAR_gcp_project_id }} TF_VAR_gcp_region: ${{ vars.TF_VAR_gcp_region }} TF_VAR_gcp_zone: ${{ vars.TF_VAR_gcp_zone }} TF_VAR_gcp_email: ${{ secrets.TF_VAR_gcp_email }} TF_VAR_gcp_keyring: ${{ secrets.TF_VAR_gcp_keyring }} TF_VAR_gcp_crypto_key: ${{ secrets.TF_VAR_gcp_crypto_key }} run: source .env && make destroy-gcp ================================================ FILE: .github/workflows/e2e.yaml ================================================ name: e2e on: workflow_dispatch: push: branches: [ 'main', 'release/**' ] pull_request: branches: [ 'main', 'release/**' ] paths-ignore: [ 'docs/**', 'rfcs/**' ] permissions: contents: read jobs: e2e-amd64-kubernetes: runs-on: group: "Default Larger Runners" labels: ubuntu-latest-16-cores services: registry: image: registry:2 ports: - 5000:5000 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: | **/go.sum **/go.mod - name: Setup Kubernetes uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: version: v0.30.0 cluster_name: kind wait: 5s config: .github/kind/config.yaml # disable KIND-net # The versions below should target the oldest supported Kubernetes version # Keep this up-to-date with https://endoflife.date/kubernetes node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64 kubectl_version: v1.33.0 - name: Setup Calico for network policy run: | kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Run tests run: make test - name: Run e2e tests run: TEST_KUBECONFIG=$HOME/.kube/config make e2e - name: Check if working tree is dirty run: | if [[ $(git diff --stat) != '' ]]; then git diff echo 'run make test and commit changes' exit 1 fi - name: Build run: make build-dev - name: flux check --pre run: | ./bin/flux check --pre - name: flux install --manifests run: | ./bin/flux install --manifests ./manifests/test/ - name: flux create secret run: | ./bin/flux create secret git git-ssh-test \ --url ssh://git@github.com/stefanprodan/podinfo ./bin/flux create secret git git-https-test \ --url https://github.com/stefanprodan/podinfo \ --username=test --password=test ./bin/flux create secret helm helm-test \ --username=test --password=test - name: flux create source git run: | ./bin/flux create source git podinfo \ --url https://github.com/stefanprodan/podinfo \ --tag-semver=">=6.3.5" - name: flux create source git export apply run: | ./bin/flux create source git podinfo-export \ --url https://github.com/stefanprodan/podinfo \ --tag-semver=">=6.3.5" \ --export | kubectl apply -f - ./bin/flux delete source git podinfo-export --silent - name: flux get sources git run: | ./bin/flux get sources git - name: flux get sources git --all-namespaces run: | ./bin/flux get sources git --all-namespaces - name: flux create kustomization run: | ./bin/flux create kustomization podinfo \ --source=podinfo \ --path="./deploy/overlays/dev" \ --prune=true \ --interval=5m \ --health-check="Deployment/frontend.dev" \ --health-check="Deployment/backend.dev" \ --health-check-timeout=3m - name: flux trace run: | ./bin/flux trace frontend \ --kind=deployment \ --api-version=apps/v1 \ --namespace=dev - name: flux reconcile kustomization --with-source run: | ./bin/flux reconcile kustomization podinfo --with-source - name: flux get kustomizations run: | ./bin/flux get kustomizations - name: flux get kustomizations --all-namespaces run: | ./bin/flux get kustomizations --all-namespaces - name: flux suspend kustomization run: | ./bin/flux suspend kustomization podinfo - name: flux resume kustomization run: | ./bin/flux resume kustomization podinfo - name: flux export run: | ./bin/flux export source git --all ./bin/flux export kustomization --all - name: flux delete kustomization run: | ./bin/flux delete kustomization podinfo --silent - name: flux create source helm run: | ./bin/flux create source helm podinfo \ --url https://stefanprodan.github.io/podinfo - name: flux create helmrelease --source=HelmRepository/podinfo run: | ./bin/flux create hr podinfo-helm \ --target-namespace=default \ --source=HelmRepository/podinfo.flux-system \ --chart=podinfo \ --chart-version=">6.0.0 <7.0.0" - name: flux create helmrelease --source=GitRepository/podinfo run: | ./bin/flux create hr podinfo-git \ --target-namespace=default \ --source=GitRepository/podinfo \ --chart=./charts/podinfo - name: flux reconcile helmrelease --with-source run: | ./bin/flux reconcile helmrelease podinfo-git --with-source - name: flux get helmreleases run: | ./bin/flux get helmreleases - name: flux get helmreleases --all-namespaces run: | ./bin/flux get helmreleases --all-namespaces - name: flux export helmrelease run: | ./bin/flux export hr --all - name: flux delete helmrelease podinfo-helm run: | ./bin/flux delete hr podinfo-helm --silent - name: flux delete helmrelease podinfo-git run: | ./bin/flux delete hr podinfo-git --silent - name: flux delete source helm run: | ./bin/flux delete source helm podinfo --silent - name: flux delete source git run: | ./bin/flux delete source git podinfo --silent - name: flux oci artifacts run: | ./bin/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ --path="./manifests" \ --source="${{ github.repositoryUrl }}" \ --revision="${{ github.ref }}@sha1:${{ github.sha }}" ./bin/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ --tag latest ./bin/flux list artifacts oci://localhost:5000/fluxcd/flux - name: flux oci repositories run: | ./bin/flux create source oci podinfo-oci \ --url oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag-semver 6.3.x \ --interval 10m ./bin/flux create kustomization podinfo-oci \ --source=OCIRepository/podinfo-oci \ --path="./" \ --prune=true \ --interval=5m \ --target-namespace=default \ --wait=true \ --health-check-timeout=3m ./bin/flux reconcile source oci podinfo-oci ./bin/flux suspend source oci podinfo-oci ./bin/flux get sources oci ./bin/flux resume source oci podinfo-oci ./bin/flux export source oci podinfo-oci ./bin/flux delete ks podinfo-oci --silent ./bin/flux delete source oci podinfo-oci --silent - name: flux create tenant run: | ./bin/flux create tenant dev-team --with-namespace=apps ./bin/flux -n apps create source helm podinfo \ --url https://stefanprodan.github.io/podinfo ./bin/flux -n apps create hr podinfo-helm \ --source=HelmRepository/podinfo \ --chart=podinfo \ --chart-version="6.3.x" \ --service-account=dev-team - name: flux2-kustomize-helm-example run: | ./bin/flux create source git flux-system \ --url=https://github.com/fluxcd/flux2-kustomize-helm-example \ --branch=main \ --ignore-paths="./clusters/**/flux-system/" \ --recurse-submodules ./bin/flux create kustomization flux-system \ --source=flux-system \ --path=./clusters/staging kubectl -n flux-system wait kustomization/infra-controllers --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m kubectl -n podinfo wait helmrelease/podinfo --for=condition=ready --timeout=5m - name: flux tree run: | ./bin/flux tree kustomization flux-system | grep Service/podinfo - name: flux events run: | ./bin/flux -n flux-system events --for Kustomization/apps | grep 'HelmRelease/podinfo' ./bin/flux -n podinfo events --for HelmRelease/podinfo | grep 'podinfo.v1' - name: flux stats run: | ./bin/flux stats -A - name: flux check run: | ./bin/flux check - name: flux migrate run: | ./bin/flux migrate - name: flux version run: | ./bin/flux version - name: flux uninstall run: | ./bin/flux uninstall --silent - name: Debug failure if: failure() run: | kubectl version --client kubectl -n flux-system get all kubectl -n flux-system describe pods kubectl -n flux-system get kustomizations -oyaml kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/kustomize-controller ================================================ FILE: .github/workflows/ossf.yaml ================================================ name: ossf on: workflow_dispatch: push: branches: - main schedule: # Weekly on Saturdays. - cron: '30 1 * * 6' permissions: read-all jobs: scorecard: runs-on: ubuntu-latest permissions: security-events: write id-token: write actions: read contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run analysis uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif repo_token: ${{ secrets.GITHUB_TOKEN }} publish_results: true - name: Upload artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif retention-days: 5 - name: Upload SARIF results uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: tags: ["v*"] permissions: contents: read jobs: release-flux-cli: outputs: hashes: ${{ steps.slsa.outputs.hashes }} image_url: ${{ steps.slsa.outputs.image_url }} image_digest: ${{ steps.slsa.outputs.image_digest }} runs-on: group: "Default Larger Runners" labels: ubuntu-latest-16-cores permissions: contents: write # needed to write releases id-token: write # needed for keyless signing packages: write # needed for ghcr access steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Unshallow run: git fetch --prune --unshallow - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache: false - name: Setup QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Setup Docker Buildx id: buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Setup Syft uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1 - name: Setup Cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 with: cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3 - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Login to GitHub Container Registry uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: fluxcdbot password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: fluxcdbot password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} - name: Generate manifests run: | make cmd/flux/.manifests.done ./manifests/scripts/bundle.sh "" ./output manifests.tar.gz kustomize build ./manifests/install > ./output/install.yaml - name: Build CRDs run: | kustomize build manifests/crds > all-crds.yaml - name: Generate OpenAPI JSON schemas from CRDs uses: fluxcd/pkg/actions/crdjsonschema@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main with: crd: all-crds.yaml output: schemas - name: Archive the OpenAPI JSON schemas run: | tar -czvf ./output/crd-schemas.tar.gz -C schemas . - name: Run GoReleaser id: run-goreleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: version: latest args: release --skip=validate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} AUR_BOT_SSH_PRIVATE_KEY: ${{ secrets.AUR_BOT_SSH_PRIVATE_KEY }} - name: Generate SLSA metadata id: slsa env: ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" run: | set -euo pipefail hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) echo "hashes=$hashes" >> $GITHUB_OUTPUT image_url=fluxcd/flux-cli:$GITHUB_REF_NAME echo "image_url=$image_url" >> $GITHUB_OUTPUT image_digest=$(docker buildx imagetools inspect ${image_url} --format '{{json .}}' | jq -r .manifest.digest) echo "image_digest=$image_digest" >> $GITHUB_OUTPUT release-flux-manifests: runs-on: ubuntu-latest needs: release-flux-cli permissions: id-token: write packages: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Kustomize uses: fluxcd/pkg/actions/kustomize@9a8c0edd5da84dc51a585738c67e3a3950d7fbf0 # main - name: Setup Flux CLI uses: ./action/ with: token: ${{ secrets.GITHUB_TOKEN }} - name: Prepare id: prep run: | VERSION=$(flux version --client | awk '{ print $NF }') echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Login to GHCR uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: fluxcdbot password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: fluxcdbot password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} - name: Push manifests to GHCR run: | mkdir -p ./ghcr.io/flux-system flux install --registry=ghcr.io/fluxcd \ --components-extra=image-reflector-controller,image-automation-controller \ --export > ./ghcr.io/flux-system/gotk-components.yaml cd ./ghcr.io && flux push artifact \ oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ --path="./flux-system" \ --source=${{ github.repositoryUrl }} \ --revision="${{ github.ref_name }}@sha1:${{ github.sha }}" - name: Push manifests to DockerHub run: | mkdir -p ./docker.io/flux-system flux install --registry=docker.io/fluxcd \ --components-extra=image-reflector-controller,image-automation-controller \ --export > ./docker.io/flux-system/gotk-components.yaml cd ./docker.io && flux push artifact \ oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ --path="./flux-system" \ --source=${{ github.repositoryUrl }} \ --revision="${{ github.ref_name }}@sha1:${{ github.sha }}" - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 with: cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3 - name: Sign manifests env: COSIGN_EXPERIMENTAL: 1 run: | cosign sign --yes ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} cosign sign --yes docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} - name: Tag manifests run: | flux tag artifact oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ --tag latest flux tag artifact oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ --tag latest release-provenance: needs: [release-flux-cli] permissions: actions: read # for detecting the Github Actions environment. id-token: write # for creating OIDC tokens for signing. contents: write # for uploading attestations to GitHub releases. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: provenance-name: "provenance.intoto.jsonl" base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}" upload-assets: true dockerhub-provenance: needs: [release-flux-cli] permissions: actions: read # for detecting the Github Actions environment. id-token: write # for creating OIDC tokens for signing. packages: write # for uploading attestations. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: ${{ needs.release-flux-cli.outputs.image_url }} digest: ${{ needs.release-flux-cli.outputs.image_digest }} registry-username: fluxcdbot secrets: registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} ghcr-provenance: needs: [release-flux-cli] permissions: actions: read # for detecting the Github Actions environment. id-token: write # for creating OIDC tokens for signing. packages: write # for uploading attestations. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 with: image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }} digest: ${{ needs.release-flux-cli.outputs.image_digest }} registry-username: fluxcdbot secrets: registry-password: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/scan.yaml ================================================ name: scan on: workflow_dispatch: push: branches: [ 'main', 'release/**' ] pull_request: branches: [ 'main', 'release/**' ] schedule: - cron: '18 10 * * 3' permissions: read-all jobs: analyze: permissions: contents: read # for reading the repository code. security-events: write # for uploading the CodeQL analysis results. uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} fossa-token: ${{ secrets.FOSSA_TOKEN }} ================================================ FILE: .github/workflows/sync-labels.yaml ================================================ name: sync-labels on: workflow_dispatch: push: branches: - main paths: - .github/labels.yaml permissions: read-all jobs: sync-labels: permissions: contents: read # for reading the labels file. issues: write # for creating and updating labels. uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/update.yaml ================================================ name: update on: workflow_dispatch: push: branches: [main] permissions: contents: read jobs: update-components: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x cache-dependency-path: | **/go.sum **/go.mod - name: Update component versions id: update env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_BODY=$(mktemp) bump_version() { local LATEST_VERSION=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name') if [[ "$LATEST_VERSION" == *"-rc"* ]]; then echo "Skipping release candidate version for $1: $LATEST_VERSION" return fi local CTRL_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p;n" manifests/bases/$1/kustomization.yaml) local CRD_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p" manifests/crds/kustomization.yaml) local API_PKG="github.com/fluxcd/$1/api" if [[ "$1" == "source-watcher" ]]; then API_PKG="github.com/fluxcd/$1/api/v2" fi local MOD_VERSION=$(go list -m -f '{{ .Version }}' "$API_PKG") local changed=false if [[ "${CTRL_VERSION}" != "${LATEST_VERSION}" ]]; then sed -i "s/\($1\/releases\/download\/\)v.*\(\/.*\)/\1${LATEST_VERSION}\2/g" "manifests/bases/$1/kustomization.yaml" changed=true fi if [[ "${CRD_VERSION}" != "${LATEST_VERSION}" ]]; then sed -i "s/\($1\/releases\/download\/\)v.*\(\/.*\)/\1${LATEST_VERSION}\2/g" "manifests/crds/kustomization.yaml" changed=true fi if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then go mod edit -require="$API_PKG@${LATEST_VERSION}" make tidy changed=true fi if [[ "$changed" == true ]]; then echo "- $1 to ${LATEST_VERSION}" >> $PR_BODY echo " https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md" >> $PR_BODY fi } { # bump controller versions bump_version helm-controller bump_version kustomize-controller bump_version source-controller bump_version notification-controller bump_version image-reflector-controller bump_version image-automation-controller bump_version source-watcher # diff change git diff # export PR_BODY for PR and commit # NB: this may look strange but it is the way it should be done to # maintain our precious newlines # Ref: https://github.com/github/docs/issues/21529 echo 'pr_body<> $GITHUB_OUTPUT cat $PR_BODY >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT } - name: Create Pull Request id: cpr uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.BOT_GITHUB_TOKEN }} commit-message: | Update toolkit components ${{ steps.update.outputs.pr_body }} committer: GitHub author: fluxcdbot signoff: true branch: update-components-${{ github.ref_name }} title: Update toolkit components body: | ${{ steps.update.outputs.pr_body }} labels: | dependencies reviewers: ${{ secrets.ASSIGNEES }} - name: Check output run: | echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" ================================================ FILE: .github/workflows/upgrade-fluxcd-pkg.yaml ================================================ name: upgrade-fluxcd-pkg on: workflow_dispatch: permissions: contents: read jobs: upgrade-fluxcd-pkg: uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0 secrets: github-token: ${{ secrets.BOT_GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Release dist/ # Dependency directories (remove the comment below to include it) # vendor/ bin/ output/ cmd/flux/manifests/ cmd/flux/.manifests.done testbin/ # Docs site/ ================================================ FILE: .goreleaser.yml ================================================ project_name: flux changelog: use: github-native builds: - <<: &build_defaults binary: flux main: ./cmd/flux ldflags: - -s -w -X main.VERSION={{ .Version }} env: - CGO_ENABLED=0 id: linux goos: - linux goarch: - amd64 - arm64 - arm goarm: - "7" - <<: *build_defaults id: darwin goos: - darwin goarch: - amd64 - arm64 - <<: *build_defaults id: windows goos: - windows archives: - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: nix builds: [linux, darwin] format: tar.gz files: - none* - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: windows builds: [windows] format: zip files: - none* source: enabled: true name_template: '{{ .ProjectName }}_{{ .Version }}_source_code' sboms: - id: source artifacts: source documents: - "{{ .ProjectName }}_{{ .Version }}_sbom.spdx.json" release: extra_files: - glob: output/crd-schemas.tar.gz - glob: output/manifests.tar.gz - glob: output/install.yaml checksum: extra_files: - glob: output/crd-schemas.tar.gz - glob: output/manifests.tar.gz - glob: output/install.yaml signs: - cmd: cosign env: - COSIGN_EXPERIMENTAL=1 certificate: '${artifact}.pem' args: - sign-blob - "--yes" - '--output-certificate=${certificate}' - '--output-signature=${signature}' - '${artifact}' artifacts: checksum output: true brews: - name: flux repository: owner: fluxcd name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" directory: Formula homepage: "https://fluxcd.io/" description: "Flux CLI" install: | bin.install "flux" generate_completions_from_executable(bin/"flux", "completion") test: | system "#{bin}/flux --version" dockers: - image_templates: - 'fluxcd/flux-cli:{{ .Tag }}-amd64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64' dockerfile: Dockerfile use: buildx goos: linux goarch: amd64 build_flag_templates: - "--pull" - "--build-arg=ARCH=linux/amd64" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.name={{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.source={{ .GitURL }}" - "--platform=linux/amd64" - image_templates: - 'fluxcd/flux-cli:{{ .Tag }}-arm64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64' dockerfile: Dockerfile use: buildx goos: linux goarch: arm64 build_flag_templates: - "--pull" - "--build-arg=ARCH=linux/arm64" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.name={{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.source={{ .GitURL }}" - "--platform=linux/arm64" - image_templates: - 'fluxcd/flux-cli:{{ .Tag }}-arm' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm' dockerfile: Dockerfile use: buildx goos: linux goarch: arm goarm: 7 build_flag_templates: - "--pull" - "--build-arg=ARCH=linux/arm" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.name={{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.source={{ .GitURL }}" - "--platform=linux/arm/v7" docker_manifests: - name_template: 'fluxcd/flux-cli:{{ .Tag }}' image_templates: - 'fluxcd/flux-cli:{{ .Tag }}-amd64' - 'fluxcd/flux-cli:{{ .Tag }}-arm64' - 'fluxcd/flux-cli:{{ .Tag }}-arm' - name_template: 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}' image_templates: - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm' docker_signs: - cmd: cosign env: - COSIGN_EXPERIMENTAL=1 args: - sign - "--yes" - '${artifact}' artifacts: all output: true ================================================ FILE: .scorecard.yml ================================================ annotations: - checks: - dangerous-workflow reasons: - reason: not-applicable # This workflow does not run untrusted code, the bot will only backport a code if the a PR was approved and merged into main. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ## Code of Conduct FluxCD toolkit follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests. This document outlines some of the conventions on to make it easier to get your contribution accepted. We gratefully welcome improvements to issues and documentation as well as to code. ## Certificate of Origin By contributing to this project you agree to the Developer Certificate of Origin (DCO). This document was created by the Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the contribution. We require all commits to be signed. By signing off with your signature, you certify that you wrote the patch or otherwise have the right to contribute the material by the rules of the [DCO](DCO): `Signed-off-by: Jane Doe ` The signature must contain your real name (sorry, no pseudonyms or anonymous contributions) If your `user.name` and `user.email` are configured in your Git config, you can sign your commit automatically with `git commit -s`. ## Communications For realtime communications we use Slack: To join the conversation, simply join the [CNCF](https://slack.cncf.io/) Slack workspace and use the [#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel. To discuss ideas and specifications we use [Github Discussions](https://github.com/fluxcd/flux2/discussions). For announcements we use a mailing list as well. Simply subscribe to [flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev) to join the conversation (there you can also add calendar invites to your Google calendar for our [Flux meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)). ## Understanding Flux and the GitOps Toolkit If you are entirely new to Flux and the GitOps Toolkit, you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI). This project is composed of: - [flux2](https://github.com/fluxcd/flux2): The Flux CLI - [source-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets) - [source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns - [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize - [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm - [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events - [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries - [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git ### Understanding the code To get started with developing controllers, you might want to review [our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which walks you through writing a short and concise controller that watches out for source changes. ## How to run the test suite Prerequisites: * go >= 1.26 * kubectl >= 1.33 * kustomize >= 5.0 Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with: ```bash make install-envtest ``` Then you can run the unit tests with: ```bash make test ``` After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine, create a cluster for testing with: ```bash make setup-kind ``` Then you can run the end-to-end tests with: ```bash make e2e ``` When the output of the Flux CLI changes, to automatically update the golden files used in the test, pass `-update` flag to the test as: ```bash make e2e TEST_ARGS="-update" ``` Since not all packages use golden files for testing, `-update` argument must be passed only for the packages that use golden files. Use the variables `TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the path of the target test package: ```bash # Unit test make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update" # e2e test make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update" ``` Teardown the e2e environment with: ```bash make cleanup-kind ``` ## Acceptance policy These things will make a PR more likely to be accepted: - a well-described requirement - tests for new code - tests for old code! - new code and tests follow the conventions in old code and tests - a good commit message (see below) - all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1) - code must build on both Linux and Darwin, via plain `go build` - code should have appropriate test coverage and tests should be written to work with `go test` In general, we will merge a PR once one maintainer has endorsed it. For substantial changes, more people may become involved, and you might get asked to resubmit the PR or divide the changes into more than one PR. ### Format of the Commit Message For the GitOps Toolkit controllers we prefer the following rules for good commit messages: - Limit the subject to 50 characters and write as the continuation of the sentence "If applied, this commit will ..." - Explain what and why in the body, if more than a trivial change; wrap it at 72 characters. The [following article](https://chris.beams.io/posts/git-commit/#seven-rules) has some more helpful advice on documenting your work. ================================================ FILE: DCO ================================================ Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ================================================ FILE: Dockerfile ================================================ FROM alpine:3.23 AS builder RUN apk add --no-cache ca-certificates curl ARG ARCH=linux/amd64 ARG KUBECTL_VER=1.35.0 RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \ -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl RUN kubectl version --client=true FROM alpine:3.23 AS flux-cli RUN apk add --no-cache ca-certificates COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/ COPY --chmod=755 flux /usr/local/bin/ USER 65534:65534 ENTRYPOINT [ "flux" ] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS ================================================ The maintainers are generally available in Slack at https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) (obtain an invitation at https://slack.cncf.io/). The Flux2 maintainers team is identical with the core maintainers of the project as listed in https://github.com/fluxcd/community/blob/main/CORE-MAINTAINERS ================================================ FILE: Makefile ================================================ VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n 1 | tr -d '"') DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s) EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig # Architecture to use envtest with ENVTEST_ARCH ?= amd64 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2)) $(filter $(subst *,%,$(2)),$(d))) all: test build tidy: go mod tidy -compat=1.26 cd tests/integration && go mod tidy -compat=1.26 fmt: go fmt ./... vet: go vet ./... setup-kind: kind create cluster --name=flux-e2e-test --kubeconfig=$(TEST_KUBECONFIG) --config=.github/kind/config.yaml kubectl --kubeconfig=$(TEST_KUBECONFIG) apply -f https://docs.projectcalico.org/v3.16/manifests/calico.yaml kubectl --kubeconfig=$(TEST_KUBECONFIG) -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true cleanup-kind: kind delete cluster --name=flux-e2e-test rm $(TEST_KUBECONFIG) KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" TEST_PKG_PATH="./..." test: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet install-envtest KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test $(TEST_PKG_PATH) -coverprofile cover.out --tags=unit $(TEST_ARGS) E2E_TEST_PKG_PATH="./cmd/flux/..." e2e: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test $(E2E_TEST_PKG_PATH) -coverprofile e2e.cover.out --tags=e2e -v -failfast $(TEST_ARGS) test-with-kind: install-envtest make setup-kind make e2e make cleanup-kind $(EMBEDDED_MANIFESTS_TARGET): $(call rwildcard,manifests/,*.yaml *.json) ./manifests/scripts/bundle.sh touch $@ build: $(EMBEDDED_MANIFESTS_TARGET) CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/flux ./cmd/flux build-dev: $(EMBEDDED_MANIFESTS_TARGET) CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(DEV_VERSION)" -o ./bin/flux ./cmd/flux .PHONY: install install: CGO_ENABLED=0 go install ./cmd/flux install-dev: CGO_ENABLED=0 go build -o /usr/local/bin ./cmd/flux setup-bootstrap-patch: go run ./tests/bootstrap/main.go setup-image-automation: cd tests/image-automation && go run main.go ENVTEST_ASSETS_DIR=$(shell pwd)/testbin ENVTEST_KUBERNETES_VERSION?=latest install-envtest: setup-envtest mkdir -p ${ENVTEST_ASSETS_DIR} $(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR) ENVTEST = $(shell pwd)/bin/setup-envtest .PHONY: envtest setup-envtest: ## Download envtest-setup locally if necessary. $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) # go-install-tool will 'go install' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) define go-install-tool @[ -f $(1) ] || { \ set -e ;\ TMP_DIR=$$(mktemp -d) ;\ cd $$TMP_DIR ;\ go mod init tmp ;\ echo "Downloading $(2)" ;\ GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ rm -rf $$TMP_DIR ;\ } endef ================================================ FILE: README.md ================================================ # Flux version 2 [![release](https://img.shields.io/github/release/fluxcd/flux2/all.svg)](https://github.com/fluxcd/flux2/releases) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://scorecard.dev/viewer/?uri=github.com/fluxcd/flux2) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2?ref=badge_shield) [![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/flux2)](https://artifacthub.io/packages/helm/fluxcd-community/flux2) [![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://fluxcd.io/flux/security/slsa-assessment) Flux is a tool for keeping Kubernetes clusters in sync with sources of configuration (like Git repositories and OCI artifacts), and automating updates to configuration when there is new code to deploy. Flux version 2 ("v2") is built from the ground up to use Kubernetes' API extension system, and to integrate with Prometheus and other core components of the Kubernetes ecosystem. In version 2, Flux supports multi-tenancy and support for syncing an arbitrary number of Git repositories, among other long-requested features. Flux v2 is constructed with the [GitOps Toolkit](#gitops-toolkit), a set of composable APIs and specialized tools for building Continuous Delivery on top of Kubernetes. Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) graduated project, used in production by various [organisations](https://fluxcd.io/adopters) and [cloud providers](https://fluxcd.io/ecosystem). ## Quickstart and documentation To get started check out this [guide](https://fluxcd.io/flux/get-started/) on how to bootstrap Flux on Kubernetes and deploy a sample application in a GitOps manner. For more comprehensive documentation, see the following guides: - [Ways of structuring your repositories](https://fluxcd.io/flux/guides/repository-structure/) - [Manage Helm Releases](https://fluxcd.io/flux/guides/helmreleases/) - [Automate image updates to Git](https://fluxcd.io/flux/guides/image-update/) - [Manage Kubernetes secrets with Flux and SOPS](https://fluxcd.io/flux/guides/mozilla-sops/) If you need help, please refer to our **[Support page](https://fluxcd.io/support/)**. ## GitOps Toolkit The GitOps Toolkit is the set of APIs and controllers that make up the runtime for Flux v2. The APIs comprise Kubernetes custom resources, which can be created and updated by a cluster user, or by other automation tooling. ![overview](https://raw.githubusercontent.com/fluxcd/flux2/main/docs/diagrams/fluxcd-controllers.png) You can use the toolkit to extend Flux, or to build your own systems for continuous delivery -- see [the developer guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/). ### Components - [Source Controllers](https://fluxcd.io/flux/components/source/) - [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/) - [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/) - [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/) - [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/) - [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/) - [ExternalArtifact CRD](https://fluxcd.io/flux/components/source/externalartifacts/) - [ArtifactGenerator CRD](https://fluxcd.io/flux/components/source/artifactgenerators/) - [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/) - [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/) - [Helm Controller](https://fluxcd.io/flux/components/helm/) - [HelmRelease CRD](https://fluxcd.io/flux/components/helm/helmreleases/) - [Notification Controller](https://fluxcd.io/flux/components/notification/) - [Provider CRD](https://fluxcd.io/flux/components/notification/providers/) - [Alert CRD](https://fluxcd.io/flux/components/notification/alerts/) - [Receiver CRD](https://fluxcd.io/flux/components/notification/receivers/) - [Image Automation Controllers](https://fluxcd.io/flux/components/image/) - [ImageRepository CRD](https://fluxcd.io/flux/components/image/imagerepositories/) - [ImagePolicy CRD](https://fluxcd.io/flux/components/image/imagepolicies/) - [ImageUpdateAutomation CRD](https://fluxcd.io/flux/components/image/imageupdateautomations/) ## Community Need help or want to contribute? Please see the links below. The Flux project is always looking for new contributors and there are a multitude of ways to get involved. - Getting Started? - Look at our [Get Started guide](https://fluxcd.io/flux/get-started/) and give us feedback - Need help? - First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions). - Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/). - Please follow our [Support Guidelines](https://fluxcd.io/support/) (in short: be nice, be respectful of volunteers' time, understand that maintainers and contributors cannot respond to all DMs, and keep discussions in the public #flux channel as much as possible). - Have feature proposals or want to contribute? - Propose features on our [GitHub Discussions page](https://github.com/fluxcd/flux2/discussions). - Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)). - [Join the flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev). - Check out [how to contribute](CONTRIBUTING.md) to the project. - Check out the [project roadmap](https://fluxcd.io/roadmap/). ### Events Check out our **[events calendar](https://fluxcd.io/#calendar)**, both with upcoming talks, events and meetings you can attend. Or view the **[resources section](https://fluxcd.io/resources)** with past events videos you can watch. We look forward to seeing you with us! ================================================ FILE: action/README.md ================================================ # Flux GitHub Action To install the latest Flux CLI on Linux, macOS or Windows GitHub runners: ```yaml steps: - name: Setup Flux CLI uses: fluxcd/flux2/action@main with: version: 'latest' - name: Run Flux CLI run: flux version --client ``` The Flux GitHub Action can be used to automate various tasks in CI, such as: - [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates) - [Push Kubernetes manifests to container registries](https://fluxcd.io/flux/flux-gh-action/#push-kubernetes-manifests-to-container-registries) - [Run end-to-end testing with Flux and Kubernetes Kind](https://fluxcd.io/flux/flux-gh-action/#end-to-end-testing) For more information, please see the [Flux GitHub Action documentation](https://fluxcd.io/flux/flux-gh-action/). ================================================ FILE: action/action.yml ================================================ name: Setup Flux CLI description: A GitHub Action for installing the Flux CLI author: Flux project branding: color: blue icon: command inputs: version: description: "Flux version e.g. 2.0.0 (defaults to latest stable release)" required: false arch: description: "arch can be amd64, arm64 or arm" required: false deprecationMessage: "No longer required, action will now detect runner arch." bindir: description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE." required: false token: description: "Token used to authenticate against the GitHub.com API." required: false runs: using: composite steps: - name: "Download the binary to the runner's cache dir" shell: bash env: VERSION: "${{ inputs.version }}" FLUX_TOOL_DIR: "${{ inputs.bindir }}" TOKEN: "${{ inputs.token }}" run: | if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then if [[ "${TOKEN}" != '' ]]; then VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4) else VERSION=$(curl -w "%{url_effective}\n" -IsSL https://github.com/fluxcd/flux2/releases/latest -o /dev/null | sed 's$^.*/$$') fi fi if [[ -z "$VERSION" ]]; then echo "Unable to determine Flux CLI version" exit 1 fi if [[ $VERSION = v* ]]; then VERSION="${VERSION:1}" fi OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]') if [[ "$OS" == "macos" ]]; then OS="darwin" fi ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]') if [[ "$ARCH" == "x64" ]]; then ARCH="amd64" elif [[ "$ARCH" == "x86" ]]; then ARCH="386" fi FLUX_EXEC_FILE="flux" if [[ "$OS" == "windows" ]]; then FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe" fi if [[ -z "$FLUX_TOOL_DIR" ]]; then FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}" fi if [[ ! -x "$FLUX_TOOL_DIR/FLUX_EXEC_FILE" ]]; then DL_DIR="$(mktemp -dt flux2-XXXXXX)" trap 'rm -rf $DL_DIR' EXIT echo "Downloading flux ${VERSION} for ${OS}/${ARCH}" FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.tar.gz" if [[ "$OS" == "windows" ]]; then FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.zip" fi FLUX_CHECKSUMS_FILE="flux_${VERSION}_checksums.txt" FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/" MAX_RETRIES=5 RETRY_DELAY=5 for i in $(seq 1 $MAX_RETRIES); do echo "Downloading flux binary (attempt $i/$MAX_RETRIES)" if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then break fi if [ $i -lt $MAX_RETRIES ]; then echo "Download failed, retrying in ${RETRY_DELAY} seconds..." sleep $RETRY_DELAY else echo "Failed to download flux binary after $MAX_RETRIES attempts" exit 1 fi done for i in $(seq 1 $MAX_RETRIES); do echo "Downloading checksums file (attempt $i/$MAX_RETRIES)" if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then break fi if [ $i -lt $MAX_RETRIES ]; then echo "Download failed, retrying in ${RETRY_DELAY} seconds..." sleep $RETRY_DELAY else echo "Failed to download checksums file after $MAX_RETRIES attempts" exit 1 fi done echo "Verifying checksum" sum="" if command -v openssl > /dev/null; then sum=$(openssl sha256 "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $2}') elif command -v sha256sum > /dev/null; then sum=$(sha256sum "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $1}') fi if [[ -z "$sum" ]]; then echo "Neither openssl nor sha256sum found. Cannot calculate checksum." exit 1 fi expected_sum=$(grep " $FLUX_TARGET_FILE\$" "$DL_DIR/$FLUX_CHECKSUMS_FILE" | awk '{print $1}') if [ "$sum" != "$expected_sum" ]; then echo "SHA sum of ${FLUX_TARGET_FILE} does not match. Aborting." exit 1 fi echo "Installing flux to ${FLUX_TOOL_DIR}" mkdir -p "$FLUX_TOOL_DIR" if [[ "$OS" == "windows" ]]; then unzip "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_EXEC_FILE" -d "$FLUX_TOOL_DIR" else tar xzf "$DL_DIR/$FLUX_TARGET_FILE" -C "$FLUX_TOOL_DIR" $FLUX_EXEC_FILE fi chmod +x "$FLUX_TOOL_DIR/$FLUX_EXEC_FILE" fi echo "Adding flux to path" echo "$FLUX_TOOL_DIR" >> "$GITHUB_PATH" - name: "Print installed flux version" shell: bash run: | flux -v ================================================ FILE: cmd/flux/alert.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) // notificationv1.Alert var alertType = apiType{ kind: notificationv1.AlertKind, humanKind: "alert", groupVersion: notificationv1.GroupVersion, } type alertAdapter struct { *notificationv1.Alert } func (a alertAdapter) asClientObject() client.Object { return a.Alert } func (a alertAdapter) deepCopyClientObject() client.Object { return a.Alert.DeepCopy() } // notificationv1.Alert type alertListAdapter struct { *notificationv1.AlertList } func (a alertListAdapter) asClientList() client.ObjectList { return a.AlertList } func (a alertListAdapter) len() int { return len(a.AlertList.Items) } ================================================ FILE: cmd/flux/alert_provider.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) // notificationv1.Provider var alertProviderType = apiType{ kind: notificationv1.ProviderKind, humanKind: "alert provider", groupVersion: notificationv1.GroupVersion, } type alertProviderAdapter struct { *notificationv1.Provider } func (a alertProviderAdapter) asClientObject() client.Object { return a.Provider } func (a alertProviderAdapter) deepCopyClientObject() client.Object { return a.Provider.DeepCopy() } // notificationv1.Provider type alertProviderListAdapter struct { *notificationv1.ProviderList } func (a alertProviderListAdapter) asClientList() client.ObjectList { return a.ProviderList } func (a alertProviderListAdapter) len() int { return len(a.ProviderList.Items) } ================================================ FILE: cmd/flux/artifact.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" ) // swapi.ArtifactGenerator var artifactGeneratorType = apiType{ kind: swapi.ArtifactGeneratorKind, humanKind: "artifactgenerator", groupVersion: swapi.GroupVersion, } type artifactGeneratorAdapter struct { *swapi.ArtifactGenerator } func (h artifactGeneratorAdapter) asClientObject() client.Object { return h.ArtifactGenerator } func (h artifactGeneratorAdapter) deepCopyClientObject() client.Object { return h.ArtifactGenerator.DeepCopy() } // swapi.ArtifactGeneratorList type artifactGeneratorListAdapter struct { *swapi.ArtifactGeneratorList } func (h artifactGeneratorListAdapter) asClientList() client.ObjectList { return h.ArtifactGeneratorList } func (h artifactGeneratorListAdapter) len() int { return len(h.ArtifactGeneratorList.Items) } ================================================ FILE: cmd/flux/bootstrap.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "crypto/elliptic" "fmt" "strings" "github.com/fluxcd/pkg/git" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var bootstrapCmd = &cobra.Command{ Use: "bootstrap", Short: "Deploy Flux on a cluster the GitOps way.", Long: `The bootstrap sub-commands push the Flux manifests to a Git repository and deploy Flux on the cluster.`, } type bootstrapFlags struct { version string logLevel flags.LogLevel branch string recurseSubmodules bool manifestsPath string defaultComponents []string extraComponents []string requiredComponents []string registry string registryCredential string imagePullSecret string secretName string tokenAuth bool keyAlgorithm flags.PublicKeyAlgorithm keyRSABits flags.RSAKeyBits keyECDSACurve flags.ECDSACurve sshHostname string caFile string privateKeyFile string sshHostKeyAlgorithms []string watchAllNamespaces bool networkPolicy bool clusterDomain string tolerationKeys []string authorName string authorEmail string gpgKeyRingPath string gpgPassphrase string gpgKeyID string force bool commitMessageAppendix string } const ( bootstrapDefaultBranch = "main" ) var bootstrapArgs = NewBootstrapFlags() func init() { bootstrapCmd.PersistentFlags().StringVarP(&bootstrapArgs.version, "version", "v", "", "toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases") bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components, "list of components, accepts comma-separated values") bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil, "list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller,source-watcher'") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", "container registry where the Flux controller images are published") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registryCredential, "registry-creds", "", "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "", "Kubernetes secret name used for pulling the controller images from a private registry") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false, "when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.watchAllNamespaces, "watch-all-namespaces", true, "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the Flux controllers are installed") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true, "setup Kubernetes network policies to deny ingress access to the Flux controllers from other namespaces") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false, "when enabled, the personal access token will be used instead of the SSH deploy key") bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.logLevel, "log-level", bootstrapArgs.logLevel.Description()) bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.tolerationKeys, "toleration-keys", nil, "list of toleration keys used to schedule the controller pods onto nodes with matching taints") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.secretName, "secret-name", rootArgs.defaults.Namespace, "name of the secret the sync credentials can be found in or stored to") bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyAlgorithm, "ssh-key-algorithm", bootstrapArgs.keyAlgorithm.Description()) bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyRSABits, "ssh-rsa-bits", bootstrapArgs.keyRSABits.Description()) bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.sshHostKeyAlgorithms, "ssh-hostkey-algos", nil, "list of host key algorithms to be used by the CLI for SSH connections") bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyECDSACurve, "ssh-ecdsa-curve", bootstrapArgs.keyECDSACurve.Description()) bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshHostname, "ssh-hostname", "", "SSH hostname, to be used when the SSH host differs from the HTTPS one") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.privateKeyFile, "private-key-file", "", "path to a private key file used for authenticating to the Git SSH server") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyRingPath, "gpg-key-ring", "", "path to GPG key ring for signing commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm") bootstrapCmd.PersistentFlags().MarkHidden("manifests") rootCmd.AddCommand(bootstrapCmd) } func NewBootstrapFlags() bootstrapFlags { return bootstrapFlags{ logLevel: flags.LogLevel(rootArgs.defaults.LogLevel), requiredComponents: []string{"source-controller", "kustomize-controller"}, keyAlgorithm: flags.PublicKeyAlgorithm(sourcesecret.ECDSAPrivateKeyAlgorithm), keyRSABits: 2048, keyECDSACurve: flags.ECDSACurve{Curve: elliptic.P384()}, } } func bootstrapComponents() []string { return append(bootstrapArgs.defaultComponents, bootstrapArgs.extraComponents...) } func buildEmbeddedManifestBase() (string, error) { if !isEmbeddedVersion(bootstrapArgs.version) { return "", nil } tmpBaseDir, err := manifestgen.MkdirTempAbs("", "flux-manifests-") if err != nil { return "", err } if err := writeEmbeddedManifests(tmpBaseDir); err != nil { return "", err } return tmpBaseDir, nil } func bootstrapValidate() error { components := bootstrapComponents() for _, component := range bootstrapArgs.requiredComponents { if !utils.ContainsItemString(components, component) { return fmt.Errorf("component %s is required", component) } } if err := utils.ValidateComponents(components); err != nil { return err } if bootstrapArgs.registryCredential != "" && bootstrapArgs.imagePullSecret == "" { return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") } if bootstrapArgs.registryCredential != "" && len(strings.Split(bootstrapArgs.registryCredential, ":")) != 2 { return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") } if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 { git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms } return nil } func mapTeamSlice(s []string, defaultPermission string) map[string]string { m := make(map[string]string, len(s)) for _, v := range s { m[v] = defaultPermission if s := strings.Split(v, ":"); len(s) == 2 { m[s[0]] = s[1] } } return m } // confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation. // It returns a nil error if Flux is not installed or the user confirms overriding an existing installation func confirmBootstrap(ctx context.Context, kubeClient client.Client) error { installed := true info, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("cluster info unavailable: %w", err) } installed = false } if installed { err = confirmFluxInstallOverride(info) if err != nil { if err == promptui.ErrAbort { return fmt.Errorf("bootstrap cancelled") } return err } } return nil } ================================================ FILE: cmd/flux/bootstrap_bitbucket_server.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "time" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var bootstrapBServerCmd = &cobra.Command{ Use: "bitbucket-server", Short: "Deploy Flux on a cluster connected to a Bitbucket Server repository", Long: `The bootstrap bitbucket-server command creates the Bitbucket Server repository if it doesn't exists and commits the Flux manifests to the master branch. Then it configures the target cluster to synchronize with the repository. If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Create a Bitbucket Server API token and export it as an env var export BITBUCKET_TOKEN= # Run bootstrap for a private repository using HTTPS token authentication flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for a private repository using SSH authentication flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --path=clusters/my-cluster # Run bootstrap for a public repository on a personal account flux bootstrap bitbucket-server --owner= --repository= --private=false --personal --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for an existing repository with a branch named main flux bootstrap bitbucket-server --owner= --username= --repository= --branch=main --hostname= --token-auth --path=clusters/my-cluster`, RunE: bootstrapBServerCmdRun, } const ( bServerDefaultPermission = "push" bServerTokenEnvVar = "BITBUCKET_TOKEN" ) type bServerFlags struct { owner string repository string interval time.Duration personal bool username string private bool hostname string path flags.SafeRelativePath teams []string readWriteKey bool reconcile bool } var bServerArgs bServerFlags func init() { bootstrapBServerCmd.Flags().StringVar(&bServerArgs.owner, "owner", "", "Bitbucket Server user or project name") bootstrapBServerCmd.Flags().StringVar(&bServerArgs.repository, "repository", "", "Bitbucket Server repository name") bootstrapBServerCmd.Flags().StringSliceVar(&bServerArgs.teams, "group", []string{}, "Bitbucket Server groups to be given write access (also accepts comma-separated values)") bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.personal, "personal", false, "if true, the owner is assumed to be a Bitbucket Server user; otherwise a group") bootstrapBServerCmd.Flags().StringVarP(&bServerArgs.username, "username", "u", "git", "authentication username") bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapBServerCmd.Flags().DurationVar(&bServerArgs.interval, "interval", time.Minute, "sync interval") bootstrapBServerCmd.Flags().StringVar(&bServerArgs.hostname, "hostname", "", "Bitbucket Server hostname") bootstrapBServerCmd.Flags().Var(&bServerArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") bootstrapCmd.AddCommand(bootstrapBServerCmd) } func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { bitbucketToken := os.Getenv(bServerTokenEnvVar) if bitbucketToken == "" { var err error bitbucketToken, err = readPasswordFromStdin("Please enter your Bitbucket personal access token (PAT): ") if err != nil { return fmt.Errorf("could not read token: %w", err) } } if bServerArgs.hostname == "" { return fmt.Errorf("invalid hostname %q", bServerArgs.hostname) } if err := bootstrapValidate(); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if !bootstrapArgs.force { err = confirmBootstrap(ctx, kubeClient) if err != nil { return err } } // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() if err != nil { return err } defer os.RemoveAll(manifestsBase) user := bServerArgs.username if bServerArgs.personal { user = bServerArgs.owner } var caBundle []byte if bootstrapArgs.caFile != "" { var err error caBundle, err = os.ReadFile(bootstrapArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } // Build Bitbucket Server provider providerCfg := provider.Config{ Provider: provider.GitProviderStash, Hostname: bServerArgs.hostname, Username: user, Token: bitbucketToken, CaBundle: caBundle, } providerClient, err := provider.BuildGitProvider(providerCfg) if err != nil { return err } // Lazy go-git repository tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ Transport: git.HTTPS, Username: user, Password: bitbucketToken, CAFile: caBundle, }, clientOpts...) if err != nil { return fmt.Errorf("failed to create a Git client: %w", err) } // Install manifest config installOptions := install.Options{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, LogLevel: bootstrapArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: rootArgs.defaults.ManifestFile, Timeout: rootArgs.timeout, TargetPath: bServerArgs.path.ToSlash(), ClusterDomain: bootstrapArgs.clusterDomain, TolerationKeys: bootstrapArgs.tolerationKeys, } if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { installOptions.BaseURL = customBaseURL } // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, Namespace: *kubeconfigArgs.Namespace, TargetPath: bServerArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { if bServerArgs.personal { secretOpts.Username = bServerArgs.owner } else { secretOpts.Username = bServerArgs.username } secretOpts.Password = bitbucketToken secretOpts.CACrt = caBundle } else { keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) if err != nil { return err } secretOpts.Keypair = keypair secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.SSHHostname = bServerArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } } // Sync manifest config syncOpts := sync.Options{ Interval: bServerArgs.interval, Name: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, Secret: bootstrapArgs.secretName, TargetPath: bServerArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, RecurseSubmodules: bootstrapArgs.recurseSubmodules, } entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if err != nil { return err } // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(bServerArgs.readWriteKey), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } if bootstrapArgs.tokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } if !bServerArgs.private { bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) } if bServerArgs.reconcile { bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) } // Setup bootstrapper with constructed configs b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) if err != nil { return err } // Run return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } ================================================ FILE: cmd/flux/bootstrap_git.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "net/url" "os" "strings" "time" "github.com/manifoldco/promptui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var bootstrapGitCmd = &cobra.Command{ Use: "git", Short: "Deploy Flux on a cluster connected to a Git repository", Long: `The bootstrap git command commits the Flux manifests to the branch of a Git repository. And then it configures the target cluster to synchronize with that repository. If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Run bootstrap for a Git repository and authenticate with your SSH agent flux bootstrap git --url=ssh://git@example.com/repository.git --path=clusters/my-cluster # Run bootstrap for a Git repository and authenticate using a password flux bootstrap git --url=https://example.com/repository.git --password= --path=clusters/my-cluster # Run bootstrap for a Git repository and authenticate using a password from environment variable GIT_PASSWORD= && flux bootstrap git --url=https://example.com/repository.git --path=clusters/my-cluster # Run bootstrap for a Git repository with a passwordless private key flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --path=clusters/my-cluster # Run bootstrap for a Git repository with a private key and password flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --password= --path=clusters/my-cluster # Run bootstrap for a Git repository on AWS CodeCommit flux bootstrap git --url=ssh://@git-codecommit..amazonaws.com/v1/repos/ --private-key-file= --password= --path=clusters/my-cluster # Run bootstrap for a Git repository on Azure Devops flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/// --private-key-file= --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster # Run bootstrap for a Git repository on Oracle VBS flux bootstrap git --url=https://repository_url.git --with-bearer-token=true --password= --path=clusters/my-cluster `, RunE: bootstrapGitCmdRun, } type gitFlags struct { url string interval time.Duration path flags.SafeRelativePath username string password string silent bool insecureHttpAllowed bool withBearerToken bool } const ( gitPasswordEnvVar = "GIT_PASSWORD" ) var gitArgs gitFlags func init() { bootstrapGitCmd.Flags().StringVar(&gitArgs.url, "url", "", "Git repository URL") bootstrapGitCmd.Flags().DurationVar(&gitArgs.interval, "interval", time.Minute, "sync interval") bootstrapGitCmd.Flags().Var(&gitArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username") bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password") bootstrapGitCmd.Flags().BoolVarP(&gitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation") bootstrapGitCmd.Flags().BoolVar(&gitArgs.insecureHttpAllowed, "allow-insecure-http", false, "allows insecure HTTP connections") bootstrapGitCmd.Flags().BoolVar(&gitArgs.withBearerToken, "with-bearer-token", false, "use password as bearer token for Authorization header") bootstrapCmd.AddCommand(bootstrapGitCmd) } func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { if gitArgs.withBearerToken { bootstrapArgs.tokenAuth = true } gitPassword := os.Getenv(gitPasswordEnvVar) if gitPassword != "" && gitArgs.password == "" { gitArgs.password = gitPassword } if bootstrapArgs.tokenAuth && gitArgs.password == "" { var err error gitPassword, err = readPasswordFromStdin("Please enter your Git repository password: ") if err != nil { return fmt.Errorf("could not read token: %w", err) } gitArgs.password = gitPassword } if err := bootstrapValidate(); err != nil { return err } repositoryURL, err := url.Parse(gitArgs.url) if err != nil { return err } if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") { if repositoryURL.Scheme == string(git.SSH) { if repositoryURL.User == nil { return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url") } if repositoryURL.User.Username() == git.DefaultPublicKeyAuthUser { return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be the SSH key ID for the provided private key") } if bootstrapArgs.privateKeyFile == "" { return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh") } } if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth { return fmt.Errorf("--token-auth=true must be specified for using an HTTPS AWS CodeCommit url") } } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if !bootstrapArgs.force { err = confirmBootstrap(ctx, kubeClient) if err != nil { return err } } // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() if err != nil { return err } defer os.RemoveAll(manifestsBase) // Lazy go-git repository tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) var caBundle []byte if bootstrapArgs.caFile != "" { var err error caBundle, err = os.ReadFile(bootstrapArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } authOpts, err := getAuthOpts(repositoryURL, caBundle) if err != nil { return fmt.Errorf("failed to create authentication options for %s: %w", repositoryURL.String(), err) } clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} if gitArgs.insecureHttpAllowed { clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP()) } gitClient, err := gogit.NewClient(tmpDir, authOpts, clientOpts...) if err != nil { return fmt.Errorf("failed to create a Git client: %w", err) } // Install manifest config installOptions := install.Options{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, LogLevel: bootstrapArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: rootArgs.defaults.ManifestFile, Timeout: rootArgs.timeout, TargetPath: gitArgs.path.ToSlash(), ClusterDomain: bootstrapArgs.clusterDomain, TolerationKeys: bootstrapArgs.tolerationKeys, } if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { installOptions.BaseURL = customBaseURL } // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, Namespace: *kubeconfigArgs.Namespace, TargetPath: gitArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { if gitArgs.withBearerToken { secretOpts.BearerToken = gitArgs.password } else { secretOpts.Username = gitArgs.username secretOpts.Password = gitArgs.password } secretOpts.CACrt = caBundle // Remove port of the given host when not syncing over HTTP/S to not assume port for protocol // This _might_ be overwritten later on by e.g. --ssh-hostname if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" { repositoryURL.Host = repositoryURL.Hostname() } // Configure repository URL to match auth config for sync. repositoryURL.User = nil if !gitArgs.insecureHttpAllowed { repositoryURL.Scheme = "https" } } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.Password = gitArgs.password secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve // Configure repository URL to match auth config for sync // Override existing user when user is not already set // or when a username was passed in if repositoryURL.User == nil || gitArgs.username != "git" { repositoryURL.User = url.User(gitArgs.username) } repositoryURL.Scheme = "ssh" if bootstrapArgs.sshHostname != "" { repositoryURL.Host = bootstrapArgs.sshHostname } keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) if err != nil { return err } secretOpts.Keypair = keypair // Configure last as it depends on the config above. secretOpts.SSHHostname = repositoryURL.Host } // Sync manifest config syncOpts := sync.Options{ Interval: gitArgs.interval, Name: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace, URL: repositoryURL.String(), Branch: bootstrapArgs.branch, Secret: bootstrapArgs.secretName, TargetPath: gitArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, RecurseSubmodules: bootstrapArgs.recurseSubmodules, } entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if err != nil { return err } // Bootstrap config bootstrapOpts := []bootstrap.GitOption{ bootstrap.WithRepositoryURL(gitArgs.url), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithLogger(logger), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } // Setup bootstrapper with constructed configs b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...) if err != nil { return err } // Run return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } // getAuthOpts retruns a AuthOptions based on the scheme // of the given URL and the configured flags. If the protocol equals // "ssh" but no private key is configured, authentication using the local // SSH-agent is attempted. func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) { switch u.Scheme { case "http": if !gitArgs.insecureHttpAllowed { return nil, fmt.Errorf("scheme http is insecure, pass --allow-insecure-http=true to allow it") } httpAuth := git.AuthOptions{ Transport: git.HTTP, } if gitArgs.withBearerToken { httpAuth.BearerToken = gitArgs.password } else { httpAuth.Username = gitArgs.username httpAuth.Password = gitArgs.password } return &httpAuth, nil case "https": httpsAuth := git.AuthOptions{ Transport: git.HTTPS, CAFile: caBundle, } if gitArgs.withBearerToken { httpsAuth.BearerToken = gitArgs.password } else { httpsAuth.Username = gitArgs.username httpsAuth.Password = gitArgs.password } return &httpsAuth, nil case "ssh": authOpts := &git.AuthOptions{ Transport: git.SSH, Username: u.User.Username(), Password: gitArgs.password, } if bootstrapArgs.privateKeyFile != "" { pk, err := os.ReadFile(bootstrapArgs.privateKeyFile) if err != nil { return nil, err } kh, err := sourcesecret.ScanHostKey(u.Host) if err != nil { return nil, err } authOpts.Identity = pk authOpts.KnownHosts = kh } return authOpts, nil default: return nil, fmt.Errorf("scheme %q is not supported", u.Scheme) } } func promptPublicKey(ctx context.Context, secret corev1.Secret, _ sourcesecret.Options) error { ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey] if !ok { return nil } logger.Successf("public key: %s", strings.TrimSpace(ppk)) if !gitArgs.silent { prompt := promptui.Prompt{ Label: "Please give the key access to your repository", IsConfirm: true, } _, err := prompt.Run() if err != nil { return fmt.Errorf("aborting") } } return nil } ================================================ FILE: cmd/flux/bootstrap_gitea.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "time" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var bootstrapGiteaCmd = &cobra.Command{ Use: "gitea", Short: "Deploy Flux on a cluster connected to a Gitea repository", Long: `The bootstrap gitea command creates the Gitea repository if it doesn't exists and commits the Flux manifests to the specified branch. Then it configures the target cluster to synchronize with that repository. If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Create a Gitea personal access token and export it as an env var export GITEA_TOKEN= # Run bootstrap for a private repository owned by a Gitea organization flux bootstrap gitea --owner= --repository= --path=clusters/my-cluster # Run bootstrap for a private repository and assign organization teams to it flux bootstrap gitea --owner= --repository= --team= --team= --path=clusters/my-cluster # Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it flux bootstrap gitea --owner= --repository= --team=: --path=clusters/my-cluster # Run bootstrap for a public repository on a personal account flux bootstrap gitea --owner= --repository= --private=false --personal=true --path=clusters/my-cluster # Run bootstrap for a private repository hosted on Gitea Enterprise using SSH auth flux bootstrap gitea --owner= --repository= --hostname= --ssh-hostname= --path=clusters/my-cluster # Run bootstrap for a private repository hosted on Gitea Enterprise using HTTPS auth flux bootstrap gitea --owner= --repository= --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for an existing repository with a branch named main flux bootstrap gitea --owner= --repository= --branch=main --path=clusters/my-cluster`, RunE: bootstrapGiteaCmdRun, } type giteaFlags struct { owner string repository string interval time.Duration personal bool private bool hostname string path flags.SafeRelativePath teams []string readWriteKey bool reconcile bool } const ( gtDefaultPermission = "maintain" gtDefaultDomain = "gitea.com" gtTokenEnvVar = "GITEA_TOKEN" ) var giteaArgs giteaFlags func init() { bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.owner, "owner", "", "Gitea user or organization name") bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.repository, "repository", "", "Gitea repository name") bootstrapGiteaCmd.Flags().StringSliceVar(&giteaArgs.teams, "team", []string{}, "Gitea team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)") bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.personal, "personal", false, "if true, the owner is assumed to be a Gitea user; otherwise an org") bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapGiteaCmd.Flags().DurationVar(&giteaArgs.interval, "interval", time.Minute, "sync interval") bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.hostname, "hostname", gtDefaultDomain, "Gitea hostname") bootstrapGiteaCmd.Flags().Var(&giteaArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") bootstrapCmd.AddCommand(bootstrapGiteaCmd) } func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { gtToken := os.Getenv(gtTokenEnvVar) if gtToken == "" { var err error gtToken, err = readPasswordFromStdin("Please enter your Gitea personal access token (PAT): ") if err != nil { return fmt.Errorf("could not read token: %w", err) } } if err := bootstrapValidate(); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() if err != nil { return err } defer os.RemoveAll(manifestsBase) var caBundle []byte if bootstrapArgs.caFile != "" { var err error caBundle, err = os.ReadFile(bootstrapArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } // Build Gitea provider providerCfg := provider.Config{ Provider: provider.GitProviderGitea, Hostname: giteaArgs.hostname, Token: gtToken, CaBundle: caBundle, } providerClient, err := provider.BuildGitProvider(providerCfg) if err != nil { return err } tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ Transport: git.HTTPS, Username: giteaArgs.owner, Password: gtToken, CAFile: caBundle, }, clientOpts...) if err != nil { return fmt.Errorf("failed to create a Git client: %w", err) } // Install manifest config installOptions := install.Options{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, LogLevel: bootstrapArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: rootArgs.defaults.ManifestFile, Timeout: rootArgs.timeout, TargetPath: giteaArgs.path.ToSlash(), ClusterDomain: bootstrapArgs.clusterDomain, TolerationKeys: bootstrapArgs.tolerationKeys, } if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { installOptions.BaseURL = customBaseURL } // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, Namespace: *kubeconfigArgs.Namespace, TargetPath: giteaArgs.path.ToSlash(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = gtToken secretOpts.CACrt = caBundle } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.SSHHostname = giteaArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } } // Sync manifest config syncOpts := sync.Options{ Interval: giteaArgs.interval, Name: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, Secret: bootstrapArgs.secretName, TargetPath: giteaArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, RecurseSubmodules: bootstrapArgs.recurseSubmodules, } entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if err != nil { return err } // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(giteaArgs.owner, giteaArgs.repository, giteaArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(giteaArgs.teams, gtDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(giteaArgs.readWriteKey), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } if bootstrapArgs.tokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } if !giteaArgs.private { bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) } if giteaArgs.reconcile { bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) } // Setup bootstrapper with constructed configs b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) if err != nil { return err } // Run return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } ================================================ FILE: cmd/flux/bootstrap_github.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "time" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var bootstrapGitHubCmd = &cobra.Command{ Use: "github", Short: "Deploy Flux on a cluster connected to a GitHub repository", Long: `The bootstrap github command creates the GitHub repository if it doesn't exists and commits the Flux manifests to the specified branch. Then it configures the target cluster to synchronize with that repository. If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Create a GitHub personal access token and export it as an env var export GITHUB_TOKEN= # Run bootstrap for a private repository owned by a GitHub organization flux bootstrap github --owner= --repository= --path=clusters/my-cluster # Run bootstrap for a private repository and assign organization teams to it flux bootstrap github --owner= --repository= --team= --team= --path=clusters/my-cluster # Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it flux bootstrap github --owner= --repository= --team=: --path=clusters/my-cluster # Run bootstrap for a public repository on a personal account flux bootstrap github --owner= --repository= --private=false --personal=true --path=clusters/my-cluster # Run bootstrap for a private repository hosted on GitHub Enterprise using SSH auth flux bootstrap github --owner= --repository= --hostname= --ssh-hostname= --path=clusters/my-cluster # Run bootstrap for a private repository hosted on GitHub Enterprise using HTTPS auth flux bootstrap github --owner= --repository= --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for an existing repository with a branch named main flux bootstrap github --owner= --repository= --branch=main --path=clusters/my-cluster`, RunE: bootstrapGitHubCmdRun, } type githubFlags struct { owner string repository string interval time.Duration personal bool private bool hostname string path flags.SafeRelativePath teams []string readWriteKey bool reconcile bool } const ( ghDefaultPermission = "maintain" ghDefaultDomain = "github.com" ghTokenEnvVar = "GITHUB_TOKEN" ) var githubArgs githubFlags func init() { bootstrapGitHubCmd.Flags().StringVar(&githubArgs.owner, "owner", "", "GitHub user or organization name") bootstrapGitHubCmd.Flags().StringVar(&githubArgs.repository, "repository", "", "GitHub repository name") bootstrapGitHubCmd.Flags().StringSliceVar(&githubArgs.teams, "team", []string{}, "GitHub team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)") bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.personal, "personal", false, "if true, the owner is assumed to be a GitHub user; otherwise an org") bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapGitHubCmd.Flags().DurationVar(&githubArgs.interval, "interval", time.Minute, "sync interval") bootstrapGitHubCmd.Flags().StringVar(&githubArgs.hostname, "hostname", ghDefaultDomain, "GitHub hostname") bootstrapGitHubCmd.Flags().Var(&githubArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") bootstrapCmd.AddCommand(bootstrapGitHubCmd) } func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { ghToken := os.Getenv(ghTokenEnvVar) if ghToken == "" { var err error ghToken, err = readPasswordFromStdin("Please enter your GitHub personal access token (PAT): ") if err != nil { return fmt.Errorf("could not read token: %w", err) } } if err := bootstrapValidate(); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if !bootstrapArgs.force { err = confirmBootstrap(ctx, kubeClient) if err != nil { return err } } // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() if err != nil { return err } defer os.RemoveAll(manifestsBase) var caBundle []byte if bootstrapArgs.caFile != "" { var err error caBundle, err = os.ReadFile(bootstrapArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } // Build GitHub provider providerCfg := provider.Config{ Provider: provider.GitProviderGitHub, Hostname: githubArgs.hostname, Token: ghToken, CaBundle: caBundle, } providerClient, err := provider.BuildGitProvider(providerCfg) if err != nil { return err } tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ Transport: git.HTTPS, Username: githubArgs.owner, Password: ghToken, CAFile: caBundle, }, clientOpts...) if err != nil { return fmt.Errorf("failed to create a Git client: %w", err) } // Install manifest config installOptions := install.Options{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, LogLevel: bootstrapArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: rootArgs.defaults.ManifestFile, Timeout: rootArgs.timeout, TargetPath: githubArgs.path.ToSlash(), ClusterDomain: bootstrapArgs.clusterDomain, TolerationKeys: bootstrapArgs.tolerationKeys, } if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { installOptions.BaseURL = customBaseURL } // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, Namespace: *kubeconfigArgs.Namespace, TargetPath: githubArgs.path.ToSlash(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = ghToken secretOpts.CACrt = caBundle } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.SSHHostname = githubArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } } // Sync manifest config syncOpts := sync.Options{ Interval: githubArgs.interval, Name: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, Secret: bootstrapArgs.secretName, TargetPath: githubArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, RecurseSubmodules: bootstrapArgs.recurseSubmodules, } entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if err != nil { return err } // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(githubArgs.readWriteKey), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } if bootstrapArgs.tokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } if !githubArgs.private { bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) } if githubArgs.reconcile { bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) } // Setup bootstrapper with constructed configs b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) if err != nil { return err } // Run return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } ================================================ FILE: cmd/flux/bootstrap_gitlab.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "regexp" "strings" "time" "github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var bootstrapGitLabCmd = &cobra.Command{ Use: "gitlab", Short: "Deploy Flux on a cluster connected to a GitLab repository", Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exist and commits the Flux manifests to the specified branch. Then it configures the target cluster to synchronize with that repository. If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Create a GitLab API token and export it as an env var export GITLAB_TOKEN= # Run bootstrap for a private repository using HTTPS token authentication flux bootstrap gitlab --owner= --repository= --token-auth # Run bootstrap for a private repository using SSH authentication flux bootstrap gitlab --owner= --repository= # Run bootstrap for a repository path flux bootstrap gitlab --owner= --repository= --path=dev-cluster # Run bootstrap for a public repository flux bootstrap gitlab --owner= --repository= --visibility=public --token-auth # Run bootstrap for a private repository hosted on a GitLab server flux bootstrap gitlab --owner= --repository= --hostname= --token-auth # Run bootstrap for an existing repository with a branch named main flux bootstrap gitlab --owner= --repository= --branch=main --token-auth # Run bootstrap for a private repository using Deploy Token authentication flux bootstrap gitlab --owner= --repository= --deploy-token-auth `, RunE: bootstrapGitLabCmdRun, } const ( glDefaultPermission = "maintain" glDefaultDomain = "gitlab.com" glTokenEnvVar = "GITLAB_TOKEN" gitlabProjectRegex = `\A[[:alnum:]\x{00A9}-\x{1f9ff}_][[:alnum:]\p{Pd}\x{00A9}-\x{1f9ff}_\.]*\z` ) type gitlabFlags struct { owner string repository string interval time.Duration personal bool visibility flags.GitLabVisibility private bool hostname string path flags.SafeRelativePath teams []string readWriteKey bool reconcile bool deployTokenAuth bool } func NewGitlabFlags() gitlabFlags { return gitlabFlags{ visibility: flags.GitLabVisibility(gitprovider.RepositoryVisibilityPrivate), } } var gitlabArgs = NewGitlabFlags() func init() { bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.owner, "owner", "", "GitLab user or group name") bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.repository, "repository", "", "GitLab repository name") bootstrapGitLabCmd.Flags().StringSliceVar(&gitlabArgs.teams, "team", []string{}, "GitLab teams to be given maintainer access (also accepts comma-separated values)") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.personal, "personal", false, "if true, the owner is assumed to be a GitLab user; otherwise a group") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapGitLabCmd.Flags().MarkDeprecated("private", "use --visibility instead") bootstrapGitLabCmd.Flags().Var(&gitlabArgs.visibility, "visibility", gitlabArgs.visibility.Description()) bootstrapGitLabCmd.Flags().DurationVar(&gitlabArgs.interval, "interval", time.Minute, "sync interval") bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.hostname, "hostname", glDefaultDomain, "GitLab hostname") bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.deployTokenAuth, "deploy-token-auth", false, "when enabled, a Project Deploy Token is generated and will be used instead of the SSH deploy token") bootstrapCmd.AddCommand(bootstrapGitLabCmd) } func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { glToken := os.Getenv(glTokenEnvVar) if glToken == "" { var err error glToken, err = readPasswordFromStdin("Please enter your GitLab personal access token (PAT): ") if err != nil { return fmt.Errorf("could not read token: %w", err) } } if projectNameIsValid, err := regexp.MatchString(gitlabProjectRegex, gitlabArgs.repository); err != nil || !projectNameIsValid { if err == nil { err = fmt.Errorf("%s is an invalid project name for gitlab.\nIt can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.", gitlabArgs.repository) } return err } if bootstrapArgs.tokenAuth && gitlabArgs.deployTokenAuth { return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.") } if !gitlabArgs.private { gitlabArgs.visibility.Set(string(gitprovider.RepositoryVisibilityPublic)) cmd.Println("Using visibility public as --private=false") } if err := bootstrapValidate(); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if !bootstrapArgs.force { err = confirmBootstrap(ctx, kubeClient) if err != nil { return err } } // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() if err != nil { return err } defer os.RemoveAll(manifestsBase) var caBundle []byte if bootstrapArgs.caFile != "" { var err error caBundle, err = os.ReadFile(bootstrapArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } // Build GitLab provider providerCfg := provider.Config{ Provider: provider.GitProviderGitLab, Hostname: gitlabArgs.hostname, Token: glToken, CaBundle: caBundle, } // Workaround for: https://github.com/fluxcd/go-git-providers/issues/55 if hostname := providerCfg.Hostname; hostname != glDefaultDomain && !strings.HasPrefix(hostname, "https://") && !strings.HasPrefix(hostname, "http://") { providerCfg.Hostname = "https://" + providerCfg.Hostname } providerClient, err := provider.BuildGitProvider(providerCfg) if err != nil { return err } // Lazy go-git repository tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ Transport: git.HTTPS, Username: gitlabArgs.owner, Password: glToken, CAFile: caBundle, }, clientOpts...) if err != nil { return fmt.Errorf("failed to create a Git client: %w", err) } // Install manifest config installOptions := install.Options{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, LogLevel: bootstrapArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: rootArgs.defaults.ManifestFile, Timeout: rootArgs.timeout, TargetPath: gitlabArgs.path.ToSlash(), ClusterDomain: bootstrapArgs.clusterDomain, TolerationKeys: bootstrapArgs.tolerationKeys, } if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { installOptions.BaseURL = customBaseURL } // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, Namespace: *kubeconfigArgs.Namespace, TargetPath: gitlabArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = glToken secretOpts.CACrt = caBundle } else if gitlabArgs.deployTokenAuth { // the actual deploy token will be reconciled later secretOpts.CACrt = caBundle } else { keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) if err != nil { return err } secretOpts.Keypair = keypair secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.SSHHostname = gitlabArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } } // Sync manifest config syncOpts := sync.Options{ Interval: gitlabArgs.interval, Name: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, Secret: bootstrapArgs.secretName, TargetPath: gitlabArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, RecurseSubmodules: bootstrapArgs.recurseSubmodules, } entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if err != nil { return err } // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal), bootstrap.WithProviderVisibility(gitlabArgs.visibility.String()), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(gitlabArgs.readWriteKey), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } if bootstrapArgs.tokenAuth || gitlabArgs.deployTokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } if gitlabArgs.deployTokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth()) } if gitlabArgs.reconcile { bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) } // Setup bootstrapper with constructed configs b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) if err != nil { return err } // Run return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } ================================================ FILE: cmd/flux/build.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var buildCmd = &cobra.Command{ Use: "build", Short: "Build a flux resource", Long: `The build command is used to build flux resources.`, } func init() { rootCmd.AddCommand(buildCmd) } ================================================ FILE: cmd/flux/build_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "bytes" "fmt" "io" "os" "strings" "github.com/spf13/cobra" "github.com/fluxcd/pkg/oci" "github.com/fluxcd/pkg/sourceignore" ) var buildArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Build artifact", Long: `The build artifact command creates a tgz file with the manifests from the given directory or a single manifest file.`, Example: ` # Build the given manifests directory into an artifact flux build artifact --path ./path/to/local/manifests --output ./path/to/artifact.tgz # Build the given single manifest file into an artifact flux build artifact --path ./path/to/local/manifest.yaml --output ./path/to/artifact.tgz # List the files bundled in the artifact tar -ztvf ./path/to/artifact.tgz `, RunE: buildArtifactCmdRun, } type buildArtifactFlags struct { output string path string ignorePaths []string } var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) var buildArtifactArgs buildArtifactFlags func init() { buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.") buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.") buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") buildCmd.AddCommand(buildArtifactCmd) } func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { if buildArtifactArgs.path == "" { return fmt.Errorf("invalid path %q", buildArtifactArgs.path) } path := buildArtifactArgs.path var err error if buildArtifactArgs.path == "-" { path, err = saveReaderToFile(os.Stdin) if err != nil { return err } defer os.Remove(path) } if _, err := os.Stat(path); err != nil { return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path) } logger.Actionf("building artifact from %s", path) ociClient := oci.NewClient(oci.DefaultOptions()) if err := ociClient.Build(buildArtifactArgs.output, path, buildArtifactArgs.ignorePaths); err != nil { return fmt.Errorf("building artifact failed, error: %w", err) } logger.Successf("artifact created at %s", buildArtifactArgs.output) return nil } func saveReaderToFile(reader io.Reader) (string, error) { b, err := io.ReadAll(bufio.NewReader(reader)) if err != nil { return "", err } b = bytes.TrimRight(b, "\r\n") f, err := os.CreateTemp("", "*.yaml") if err != nil { return "", fmt.Errorf("unable to create temp dir for stdin") } defer f.Close() if _, err := f.Write(b); err != nil { return "", fmt.Errorf("error writing stdin to file: %w", err) } return f.Name(), nil } ================================================ FILE: cmd/flux/build_artifact_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "os" "strings" "testing" . "github.com/onsi/gomega" ) func Test_saveReaderToFile(t *testing.T) { g := NewWithT(t) testString := `apiVersion: v1 kind: ConfigMap metadata: name: myapp data: foo: bar` tests := []struct { name string string string expectErr bool }{ { name: "yaml", string: testString, }, { name: "yaml with carriage return", string: testString + "\r\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tmpFile, err := saveReaderToFile(strings.NewReader(tt.string)) g.Expect(err).To(BeNil()) t.Cleanup(func() { _ = os.Remove(tmpFile) }) b, err := os.ReadFile(tmpFile) if tt.expectErr { g.Expect(err).To(Not(BeNil())) return } g.Expect(err).To(BeNil()) g.Expect(string(b)).To(BeEquivalentTo(testString)) }) } } ================================================ FILE: cmd/flux/build_kustomization.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "os/signal" "path/filepath" "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/internal/build" ) var buildKsCmd = &cobra.Command{ Use: "kustomization", Aliases: []string{"ks"}, Short: "Build Kustomization", Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout. It is possible to specify a Flux kustomization file using --kustomization-file.`, Example: `# Build the local manifests as they were built on the cluster flux build kustomization my-app --path ./path/to/local/manifests # Build using a local flux kustomization file flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml # Build in dry-run mode without connecting to the cluster. # Note that variable substitutions from Secrets and ConfigMaps are skipped in dry-run mode. flux build kustomization my-app --path ./path/to/local/manifests \ --kustomization-file ./path/to/local/my-app.yaml \ --dry-run # Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. flux build kustomization my-app --path ./path/to/local/manifests \ --kustomization-file ./path/to/local/my-app.yaml \ --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml" # Run recursively on all encountered Kustomizations flux build kustomization my-app --path ./path/to/local/manifests \ --recursive \ --local-sources GitRepository/flux-system/my-repo=./path/to/local/git`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, } type buildKsFlags struct { kustomizationFile string path string ignorePaths []string dryRun bool strictSubst bool recursive bool localSources map[string]string } var buildKsArgs buildKsFlags func init() { buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") buildKsCmd.Flags().BoolVar(&buildKsArgs.strictSubst, "strict-substitute", false, "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations") buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path") buildCmd.AddCommand(buildKsCmd) } func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { if len(args) < 1 { return fmt.Errorf("%s name is required", kustomizationType.humanKind) } name := args[0] if buildKsArgs.path == "" { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } // Normalize the path to handle Windows absolute and relative paths correctly buildKsArgs.path, err = filepath.Abs(buildKsArgs.path) if err != nil { return fmt.Errorf("failed to resolve absolute path: %w", err) } buildKsArgs.path = filepath.Clean(buildKsArgs.path) if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" { return fmt.Errorf("dry-run mode requires a kustomization file") } if buildKsArgs.kustomizationFile != "" { if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) } } var builder *build.Builder if buildKsArgs.dryRun { builder, err = build.NewBuilder(name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(buildKsArgs.kustomizationFile), build.WithDryRun(buildKsArgs.dryRun), build.WithNamespace(*kubeconfigArgs.Namespace), build.WithIgnore(buildKsArgs.ignorePaths), build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithRecursive(buildKsArgs.recursive), build.WithLocalSources(buildKsArgs.localSources), ) } else { builder, err = build.NewBuilder(name, buildKsArgs.path, build.WithClientConfig(kubeconfigArgs, kubeclientOptions), build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(buildKsArgs.kustomizationFile), build.WithIgnore(buildKsArgs.ignorePaths), build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithRecursive(buildKsArgs.recursive), build.WithLocalSources(buildKsArgs.localSources), ) } if err != nil { return err } // create a signal channel sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt) errChan := make(chan error) go func() { objects, err := builder.Build() if err != nil { errChan <- err } manifests, err := ssautil.ObjectsToYAML(objects) if err != nil { errChan <- err } cmd.Print(manifests) errChan <- nil }() select { case <-sigc: fmt.Println("Build cancelled... exiting.") return builder.Cancel() case err := <-errChan: if err != nil { return err } } return nil } ================================================ FILE: cmd/flux/build_kustomization_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "os" "path/filepath" "testing" "text/template" ) func setup(t *testing.T, tmpl map[string]string) { t.Helper() testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t) } func TestBuildKustomization(t *testing.T) { tests := []struct { name string args string resultFile string assertFunc string }{ { name: "no args", args: "build kustomization podinfo", resultFile: "invalid resource path \"\"", assertFunc: "assertError", }, { name: "build podinfo", args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo", resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build podinfo without service", args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build deployment and configmap with var substitution", args: "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution", resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build ignore", args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\"", resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build with recursive", args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization", resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", assertFunc: "assertGoldenTemplateFile", }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setup(t, tmpl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var assert assertFunc switch tt.assertFunc { case "assertGoldenTemplateFile": assert = assertGoldenTemplateFile(tt.resultFile, tmpl) case "assertError": assert = assertError(tt.resultFile) } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: assert, } cmd.runTestCmd(t) }) } } func TestBuildLocalKustomization(t *testing.T) { podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 5m0s path: ./kustomize force: true prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default postBuild: substitute: cluster_env: "prod" cluster_region: "eu-central-1" ` tmpFile := filepath.Join(t.TempDir(), "podinfo.yaml") tests := []struct { name string args string resultFile string assertFunc string }{ { name: "no args", args: "build kustomization podinfo --kustomization-file ./wrongfile/ --path ./testdata/build-kustomization/podinfo", resultFile: "invalid kustomization file \"./wrongfile/\"", assertFunc: "assertError", }, { name: "build podinfo", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo", resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build podinfo without service", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service", resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build deployment and configmap with var substitution", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution", resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build deployment and configmap with var substitution in dry-run mode", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution --dry-run", resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build with recursive", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization", resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build with recursive in dry-run mode", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --dry-run", resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", assertFunc: "assertGoldenTemplateFile", }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setup(t, tmpl) temp, err := template.New("podinfo").Parse(podinfo) if err != nil { t.Fatal(err) } var b bytes.Buffer err = temp.Execute(&b, tmpl) if err != nil { t.Fatal(err) } err = os.WriteFile(tmpFile, b.Bytes(), 0666) if err != nil { t.Fatal(err) } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var assert assertFunc switch tt.assertFunc { case "assertGoldenTemplateFile": assert = assertGoldenTemplateFile(tt.resultFile, tmpl) case "assertError": assert = assertError(tt.resultFile) } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: assert, } cmd.runTestCmd(t) }) } } // TestBuildKustomizationPathNormalization verifies that absolute and complex // paths are normalized to prevent path concatenation bugs (issue #5673). // Without normalization, paths could be duplicated like: /path/test/path/test/file func TestBuildKustomizationPathNormalization(t *testing.T) { // Get absolute path to testdata to test absolute path handling absTestDataPath, err := filepath.Abs("testdata/build-kustomization/podinfo") if err != nil { t.Fatalf("failed to get absolute path: %v", err) } tests := []struct { name string args string resultFile string assertFunc string }{ { name: "build with absolute path", args: "build kustomization podinfo --path " + absTestDataPath, resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build with complex relative path (parent dir)", args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo", resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, { name: "build with path containing redundant separators", args: "build kustomization podinfo --path ./testdata//build-kustomization//podinfo", resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setup(t, tmpl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var assert assertFunc switch tt.assertFunc { case "assertGoldenTemplateFile": assert = assertGoldenTemplateFile(tt.resultFile, tmpl) case "assertError": assert = assertError(tt.resultFile) } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/check.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "time" "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/version" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/status" ) var checkCmd = &cobra.Command{ Use: "check", Args: cobra.NoArgs, Short: "Check requirements and installation", Long: withPreviewNote(`The check command will perform a series of checks to validate that the local environment is configured correctly and if the installed components are healthy.`), Example: ` # Run pre-installation checks flux check --pre # Run installation checks flux check`, RunE: runCheckCmd, } type checkFlags struct { pre bool components []string extraComponents []string pollInterval time.Duration } var kubernetesConstraints = []string{ ">=1.33.0-0", } var checkArgs checkFlags func init() { checkCmd.Flags().BoolVarP(&checkArgs.pre, "pre", "", false, "only run pre-installation checks") checkCmd.Flags().StringSliceVar(&checkArgs.components, "components", rootArgs.defaults.Components, "list of components, accepts comma-separated values") checkCmd.Flags().StringSliceVar(&checkArgs.extraComponents, "components-extra", nil, "list of components in addition to those supplied or defaulted, accepts comma-separated values") checkCmd.Flags().DurationVar(&checkArgs.pollInterval, "poll-interval", 5*time.Second, "how often the health checker should poll the cluster for the latest state of the resources.") rootCmd.AddCommand(checkCmd) } func runCheckCmd(cmd *cobra.Command, args []string) error { logger.Actionf("checking prerequisites") checkFailed := false fluxCheck() ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return fmt.Errorf("Kubernetes client initialization failed: %s", err.Error()) } kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()}) if err != nil { return err } if !kubernetesCheck(cfg, kubernetesConstraints) { checkFailed = true } if checkArgs.pre { if checkFailed { os.Exit(1) } logger.Successf("prerequisites checks passed") return nil } logger.Actionf("checking version in cluster") if !fluxClusterVersionCheck(ctx, kubeClient) { checkFailed = true } logger.Actionf("checking controllers") if !componentsCheck(ctx, kubeClient) { checkFailed = true } logger.Actionf("checking crds") if !crdsCheck(ctx, kubeClient) { checkFailed = true } if checkFailed { logger.Failuref("check failed") os.Exit(1) } logger.Successf("all checks passed") return nil } func fluxCheck() { curSv, err := version.ParseVersion(VERSION) if err != nil { return } // Exclude development builds. if curSv.Prerelease() != "" { return } latest, err := install.GetLatestVersion() if err != nil { return } latestSv, err := version.ParseVersion(latest) if err != nil { return } if latestSv.GreaterThan(curSv) { logger.Failuref("flux %s <%s (new CLI version is available, please upgrade)", curSv, latestSv) } } func kubernetesCheck(cfg *rest.Config, constraints []string) bool { clientSet, err := kubernetes.NewForConfig(cfg) if err != nil { logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) return false } kv, err := clientSet.Discovery().ServerVersion() if err != nil { logger.Failuref("Kubernetes API call failed: %s", err.Error()) return false } v, err := version.ParseVersion(kv.String()) if err != nil { logger.Failuref("Kubernetes version can't be determined") return false } var valid bool var vrange string for _, constraint := range constraints { c, _ := semver.NewConstraint(constraint) if c.Check(v) { valid = true vrange = constraint break } } if !valid { logger.Failuref("Kubernetes version %s does not match %s", v.Original(), constraints[0]) return false } logger.Successf("Kubernetes %s %s", v.String(), vrange) return true } func componentsCheck(ctx context.Context, kubeClient client.Client) bool { statusChecker, err := status.NewStatusCheckerWithClient(kubeClient, checkArgs.pollInterval, rootArgs.timeout, logger) if err != nil { return false } ok := true selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} var list v1.DeploymentList ns := *kubeconfigArgs.Namespace if err := kubeClient.List(ctx, &list, client.InNamespace(ns), selector); err == nil { if len(list.Items) == 0 { logger.Failuref("no controllers found in the '%s' namespace with the label selector '%s=%s'", ns, manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue) return false } for _, d := range list.Items { if ref, err := buildComponentObjectRefs(d.Name); err == nil { if err := statusChecker.Assess(ref...); err != nil { ok = false } } for _, c := range d.Spec.Template.Spec.Containers { logger.Actionf("%s", c.Image) } } } return ok } func crdsCheck(ctx context.Context, kubeClient client.Client) bool { ok := true selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} var list apiextensionsv1.CustomResourceDefinitionList if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err == nil { if len(list.Items) == 0 { logger.Failuref("no crds found with the label selector '%s=%s'", manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue) return false } for _, crd := range list.Items { versions := crd.Status.StoredVersions if len(versions) > 0 { logger.Successf("%s", crd.Name+"/"+versions[len(versions)-1]) } else { ok = false logger.Failuref("no stored versions for %s", crd.Name) } } } return ok } func fluxClusterVersionCheck(ctx context.Context, kubeClient client.Client) bool { clusterInfo, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { logger.Failuref("checking failed: %s", err.Error()) return false } if clusterInfo.distribution() != "" { logger.Successf("distribution: %s", clusterInfo.distribution()) } logger.Successf("bootstrapped: %t", clusterInfo.bootstrapped) return true } ================================================ FILE: cmd/flux/check_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "strings" "testing" "github.com/fluxcd/flux2/v2/internal/utils" ) func TestCheckPre(t *testing.T) { jsonOutput, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, "version", "--output", "json") if err != nil { t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error()) } var versions map[string]interface{} if err := json.Unmarshal([]byte(jsonOutput), &versions); err != nil { t.Fatalf("Error unmarshalling '%s': %v", jsonOutput, err.Error()) } serverGitVersion := strings.TrimPrefix( versions["serverVersion"].(map[string]interface{})["gitVersion"].(string), "v") cmd := cmdTestCase{ args: "check --pre", assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{ "serverVersion": serverGitVersion, }), } cmd.runTestCmd(t) } ================================================ FILE: cmd/flux/cluster_info.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/manifoldco/promptui" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) // bootstrapLabels are labels put on a resource by kustomize-controller. These labels on the CRD indicates // that flux has been bootstrapped. var bootstrapLabels = []string{ fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group), fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group), } // fluxClusterInfo contains information about an existing flux installation on a cluster. type fluxClusterInfo struct { // bootstrapped indicates that Flux was installed using the `flux bootstrap` command. bootstrapped bool // managedBy is the name of the tool being used to manage the installation of Flux. managedBy string // partOf indicates which distribution the instance is a part of. partOf string // version is the Flux version number in semver format. version string } // getFluxClusterInfo returns information on the Flux installation running on the cluster. // If an error occurred, the returned error will be non-nil. // // This function retrieves the GitRepository CRD from the cluster and checks it // for a set of labels used to determine the Flux version and how Flux was installed. // It returns the NotFound error from the underlying library if it was unable to find // the GitRepository CRD and this can be used to check if Flux is installed. func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) { var info fluxClusterInfo crdMetadata := &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ APIVersion: apiextensionsv1.SchemeGroupVersion.String(), Kind: "CustomResourceDefinition", }, ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("gitrepositories.%s", sourcev1.GroupVersion.Group), }, } if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil { return info, err } info.version = crdMetadata.Labels[manifestgen.VersionLabelKey] var present bool for _, l := range bootstrapLabels { _, present = crdMetadata.Labels[l] } if present { info.bootstrapped = true } // the `app.kubernetes.io/managed-by` label is not set by flux but might be set by other // tools used to install Flux e.g Helm. if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok { info.managedBy = manager } if partOf, ok := crdMetadata.Labels[manifestgen.PartOfLabelKey]; ok { info.partOf = partOf } return info, nil } // confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding // a Flux installation. It returns nil if the installation should continue, // promptui.ErrAbort if the user doesn't confirm, or an error encountered. func confirmFluxInstallOverride(info fluxClusterInfo) error { // no need to display prompt if installation is managed by Flux if installManagedByFlux(info.managedBy) { return nil } display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy) fmt.Fprintln(rootCmd.ErrOrStderr(), display) prompt := promptui.Prompt{ Label: fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy), IsConfirm: true, } _, err := prompt.Run() return err } func (info fluxClusterInfo) distribution() string { distribution := info.version if info.partOf != "" { distribution = fmt.Sprintf("%s-%s", info.partOf, info.version) } return distribution } func installManagedByFlux(manager string) bool { return manager == "" || manager == "flux" } ================================================ FILE: cmd/flux/cluster_info_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "testing" . "github.com/onsi/gomega" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ssautil "github.com/fluxcd/pkg/ssa/utils" ) func Test_getFluxClusterInfo(t *testing.T) { g := NewWithT(t) f, err := os.Open("./testdata/cluster_info/gitrepositories.yaml") g.Expect(err).To(BeNil()) objs, err := ssautil.ReadObjects(f) g.Expect(err).To(Not(HaveOccurred())) gitrepo := objs[0] tests := []struct { name string labels map[string]string wantErr bool wantInfo fluxClusterInfo }{ { name: "no git repository CRD present", wantErr: true, }, { name: "CRD with kustomize-controller labels", labels: map[string]string{ fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system", fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", "app.kubernetes.io/version": "v2.1.0", }, wantInfo: fluxClusterInfo{ version: "v2.1.0", bootstrapped: true, }, }, { name: "CRD with kustomize-controller labels and managed-by label", labels: map[string]string{ fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system", fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", "app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/managed-by": "flux", }, wantInfo: fluxClusterInfo{ version: "v2.1.0", bootstrapped: true, managedBy: "flux", }, }, { name: "CRD with only managed-by label", labels: map[string]string{ "app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/managed-by": "helm", }, wantInfo: fluxClusterInfo{ version: "v2.1.0", managedBy: "helm", }, }, { name: "CRD with no labels", labels: map[string]string{}, wantInfo: fluxClusterInfo{}, }, { name: "CRD with only version label", labels: map[string]string{ "app.kubernetes.io/version": "v2.1.0", }, wantInfo: fluxClusterInfo{ version: "v2.1.0", }, }, { name: "CRD with version and part-of labels", labels: map[string]string{ "app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/part-of": "flux", }, wantInfo: fluxClusterInfo{ version: "v2.1.0", partOf: "flux", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) newscheme := runtime.NewScheme() apiextensionsv1.AddToScheme(newscheme) builder := fake.NewClientBuilder().WithScheme(newscheme) if tt.labels != nil { gitrepo.SetLabels(tt.labels) builder = builder.WithRuntimeObjects(gitrepo) } client := builder.Build() info, err := getFluxClusterInfo(context.Background(), client) if tt.wantErr { g.Expect(err).To(HaveOccurred()) g.Expect(errors.IsNotFound(err)).To(BeTrue()) } else { g.Expect(err).To(Not(HaveOccurred())) } g.Expect(info).To(BeEquivalentTo(tt.wantInfo)) }) } } ================================================ FILE: cmd/flux/completion.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "strings" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" ) var completionCmd = &cobra.Command{ Use: "completion", Short: "Generates completion scripts for various shells", Long: `The completion sub-command generates completion scripts for various shells.`, } func init() { rootCmd.AddCommand(completionCmd) } func contextsCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { rawConfig, err := kubeconfigArgs.ToRawKubeConfigLoader().RawConfig() if err != nil { return completionError(err) } var comps []string for name := range rawConfig.Contexts { if strings.HasPrefix(name, toComplete) { comps = append(comps, name) } } return comps, cobra.ShellCompDirectiveNoFileComp } func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return completionError(err) } mapper, err := kubeconfigArgs.ToRESTMapper() if err != nil { return completionError(err) } mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { return completionError(err) } client, err := dynamic.NewForConfig(cfg) if err != nil { return completionError(err) } var dr dynamic.ResourceInterface if mapping.Scope.Name() == meta.RESTScopeNameNamespace { dr = client.Resource(mapping.Resource).Namespace(*kubeconfigArgs.Namespace) } else { dr = client.Resource(mapping.Resource) } list, err := dr.List(ctx, metav1.ListOptions{}) if err != nil { return completionError(err) } var comps []string for _, item := range list.Items { name := item.GetName() if strings.HasPrefix(name, toComplete) { comps = append(comps, name) } } return comps, cobra.ShellCompDirectiveNoFileComp } } func completionError(err error) ([]string, cobra.ShellCompDirective) { cobra.CompError(err.Error()) return nil, cobra.ShellCompDirectiveError } ================================================ FILE: cmd/flux/completion_bash.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "os" "github.com/spf13/cobra" ) var completionBashCmd = &cobra.Command{ Use: "bash", Short: "Generates bash completion scripts", Long: `The completion sub-command generates completion scripts for bash.`, Example: `To load completion run . <(flux completion bash) To configure your bash shell to load completions for each session add to your bashrc # ~/.bashrc or ~/.profile command -v flux >/dev/null && . <(flux completion bash)`, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenBashCompletion(os.Stdout) }, } func init() { completionCmd.AddCommand(completionBashCmd) } ================================================ FILE: cmd/flux/completion_fish.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "os" "github.com/spf13/cobra" ) var completionFishCmd = &cobra.Command{ Use: "fish", Short: "Generates fish completion scripts", Long: `The completion sub-command generates completion scripts for fish.`, Example: `To configure your fish shell to load completions for each session write this script to your completions dir: flux completion fish > ~/.config/fish/completions/flux.fish See http://fishshell.com/docs/current/index.html#completion-own for more details`, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenFishCompletion(os.Stdout, true) }, } func init() { completionCmd.AddCommand(completionFishCmd) } ================================================ FILE: cmd/flux/completion_powershell.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "os" "github.com/spf13/cobra" ) var completionPowerShellCmd = &cobra.Command{ Use: "powershell", Short: "Generates powershell completion scripts", Long: `The completion sub-command generates completion scripts for powershell.`, Example: `To load completion run . <(flux completion powershell) To configure your powershell shell to load completions for each session add to your powershell profile Windows: cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" flux completion powershell >> flux-completion.ps1 Linux: cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules" flux completion powershell >> flux-completions.ps1`, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenPowerShellCompletion(os.Stdout) }, } func init() { completionCmd.AddCommand(completionPowerShellCmd) } ================================================ FILE: cmd/flux/completion_zsh.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "github.com/spf13/cobra" ) var completionZshCmd = &cobra.Command{ Use: "zsh", Short: "Generates zsh completion scripts", Long: `The completion sub-command generates completion scripts for zsh.`, Example: `To load completion run . <(flux completion zsh) To configure your zsh shell to load completions for each session add to your zshrc # ~/.zshrc or ~/.profile command -v flux >/dev/null && . <(flux completion zsh) or write a cached file in one of the completion directories in your ${fpath}: echo "${fpath// /\n}" | grep -i completion flux completion zsh > _flux mv _flux ~/.oh-my-zsh/completions # oh-my-zsh mv _flux ~/.zprezto/modules/completion/external/src/ # zprezto`, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenZshCompletion(os.Stdout) // Cobra doesn't source zsh completion file, explicitly doing it here fmt.Println("compdef _flux flux") }, } func init() { completionCmd.AddCommand(completionZshCmd) } ================================================ FILE: cmd/flux/create.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "regexp" "strings" "time" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/fluxcd/flux2/v2/internal/utils" ) var createCmd = &cobra.Command{ Use: "create", Short: "Create or update sources and resources", Long: `The create sub-commands generate sources and resources.`, } type createFlags struct { interval time.Duration export bool labels []string } var createArgs createFlags func init() { createCmd.PersistentFlags().DurationVarP(&createArgs.interval, "interval", "", time.Minute, "source sync interval") createCmd.PersistentFlags().BoolVar(&createArgs.export, "export", false, "export in YAML format to stdout") createCmd.PersistentFlags().StringSliceVar(&createArgs.labels, "label", nil, "set labels on the resource (can specify multiple labels with commas: label1=value1,label2=value2)") createCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("name is required") } name := args[0] if !validateObjectName(name) { return fmt.Errorf("name '%s' is invalid, it should adhere to standard defined in RFC 1123, the name can only contain alphanumeric characters or '-'", name) } return nil } rootCmd.AddCommand(createCmd) } // upsertable is an interface for values that can be used in `upsert`. type upsertable interface { adapter named } // upsert updates or inserts an object. Instead of providing the // object itself, you provide a named (as in Name and Namespace) // template value, and a mutate function which sets the values you // want to update. The mutate function is nullary -- you mutate a // value in the closure, e.g., by doing this: // // var existing Value // existing.Name = name // existing.Namespace = ns // upsert(ctx, client, valueAdapter{&value}, func() error { // value.Spec = onePreparedEarlier // }) func (names apiType) upsert(ctx context.Context, kubeClient client.Client, object upsertable, mutate func() error) (types.NamespacedName, error) { nsname := types.NamespacedName{ Namespace: object.GetNamespace(), Name: object.GetName(), } op, err := controllerutil.CreateOrUpdate(ctx, kubeClient, object.asClientObject(), mutate) if err != nil { return nsname, err } switch op { case controllerutil.OperationResultCreated: logger.Successf("%s created", names.kind) case controllerutil.OperationResultUpdated: logger.Successf("%s updated", names.kind) } return nsname, nil } type upsertWaitable interface { upsertable statusable } // upsertAndWait encodes the pattern of creating or updating a // resource, then waiting for it to reconcile. See the note on // `upsert` for how to work with the `mutate` argument. func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) // NB globals if err != nil { return err } logger.Generatef("generating %s", names.kind) logger.Actionf("applying %s", names.kind) namespacedName, err := names.upsert(ctx, kubeClient, object, mutate) if err != nil { return err } logger.Waitingf("waiting for %s reconciliation", names.kind) if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, object.asClientObject())); err != nil { return err } logger.Successf("%s reconciliation completed", names.kind) return nil } func parseLabels() (map[string]string, error) { result := make(map[string]string) for _, label := range createArgs.labels { // validate key value pair parts := strings.Split(label, "=") if len(parts) != 2 { return nil, fmt.Errorf("invalid label format '%s', must be key=value", label) } // validate label name if errors := validation.IsQualifiedName(parts[0]); len(errors) > 0 { return nil, fmt.Errorf("invalid label '%s': %v", parts[0], errors) } // validate label value if errors := validation.IsValidLabelValue(parts[1]); len(errors) > 0 { return nil, fmt.Errorf("invalid label value '%s': %v", parts[1], errors) } result[parts[0]] = parts[1] } return result, nil } func validateObjectName(name string) bool { r := regexp.MustCompile(`^[a-z0-9]([a-z0-9\-]){0,61}[a-z0-9]$`) return r.MatchString(name) } ================================================ FILE: cmd/flux/create_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" ) var createAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Create or update a Alert resource", Long: withPreviewNote(`The create alert command generates a Alert resource.`), Example: ` # Create an Alert for kustomization events flux create alert \ --event-severity info \ --event-source Kustomization/flux-system \ --provider-ref slack \ flux-system`, RunE: createAlertCmdRun, } type alertFlags struct { providerRef string eventSeverity string eventSources []string } var alertArgs alertFlags func init() { createAlertCmd.Flags().StringVar(&alertArgs.providerRef, "provider-ref", "", "reference to provider") createAlertCmd.Flags().StringVar(&alertArgs.eventSeverity, "event-severity", "", "severity of events to send alerts for") createAlertCmd.Flags().StringSliceVar(&alertArgs.eventSources, "event-source", []string{}, "sources that should generate alerts (/), also accepts comma-separated values") createCmd.AddCommand(createAlertCmd) } func createAlertCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if alertArgs.providerRef == "" { return fmt.Errorf("provider ref is required") } eventSources := []notificationv1.CrossNamespaceObjectReference{} for _, eventSource := range alertArgs.eventSources { kind, name, namespace := utils.ParseObjectKindNameNamespace(eventSource) if kind == "" { return fmt.Errorf("invalid event source '%s', must be in format /", eventSource) } eventSources = append(eventSources, notificationv1.CrossNamespaceObjectReference{ Kind: kind, Name: name, Namespace: namespace, }) } if len(eventSources) == 0 { return fmt.Errorf("at least one event source is required") } sourceLabels, err := parseLabels() if err != nil { return err } if !createArgs.export { logger.Generatef("generating Alert") } alert := notificationv1b3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: notificationv1b3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: alertArgs.providerRef, }, EventSeverity: alertArgs.eventSeverity, EventSources: eventSources, Suspend: false, }, } if createArgs.export { return printExport(exportAlert(&alert)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying Alert") namespacedName, err := upsertAlert(ctx, kubeClient, &alert) if err != nil { return err } logger.Waitingf("waiting for Alert reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &alert)); err != nil { return err } logger.Successf("Alert %s is ready", name) return nil } func upsertAlert(ctx context.Context, kubeClient client.Client, alert *notificationv1b3.Alert) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: alert.GetNamespace(), Name: alert.GetName(), } var existing notificationv1b3.Alert err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, alert); err != nil { return namespacedName, err } else { logger.Successf("Alert created") return namespacedName, nil } } return namespacedName, err } existing.Labels = alert.Labels existing.Spec = alert.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } alert = &existing logger.Successf("Alert updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_alertprovider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" ) var createAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Create or update a Provider resource", Long: withPreviewNote(`The create alert-provider command generates a Provider resource.`), Example: ` # Create a Provider for a Slack channel flux create alert-provider slack \ --type slack \ --channel general \ --address https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ --secret-ref webhook-url # Create a Provider for a Github repository flux create alert-provider github-podinfo \ --type github \ --address https://github.com/stefanprodan/podinfo \ --secret-ref github-token`, RunE: createAlertProviderCmdRun, } type alertProviderFlags struct { alertType string channel string username string address string secretRef string } var alertProviderArgs alertProviderFlags func init() { createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.alertType, "type", "", "type of provider") createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.channel, "channel", "", "channel to send messages to in the case of a chat provider") createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.username, "username", "", "bot username used by the provider") createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.address, "address", "", "path to either the git repository, chat provider or webhook") createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.secretRef, "secret-ref", "", "name of secret containing authentication token") createCmd.AddCommand(createAlertProviderCmd) } func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if alertProviderArgs.alertType == "" { return fmt.Errorf("Provider type is required") } sourceLabels, err := parseLabels() if err != nil { return err } if !createArgs.export { logger.Generatef("generating Provider") } provider := notificationv1.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: notificationv1.ProviderSpec{ Type: alertProviderArgs.alertType, Channel: alertProviderArgs.channel, Username: alertProviderArgs.username, Address: alertProviderArgs.address, }, } if alertProviderArgs.secretRef != "" { provider.Spec.SecretRef = &meta.LocalObjectReference{ Name: alertProviderArgs.secretRef, } } if createArgs.export { return printExport(exportAlertProvider(&provider)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying Provider") namespacedName, err := upsertAlertProvider(ctx, kubeClient, &provider) if err != nil { return err } logger.Waitingf("waiting for Provider reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &provider)); err != nil { return err } logger.Successf("Provider %s is ready", name) return nil } func upsertAlertProvider(ctx context.Context, kubeClient client.Client, provider *notificationv1.Provider) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: provider.GetNamespace(), Name: provider.GetName(), } var existing notificationv1.Provider err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, provider); err != nil { return namespacedName, err } else { logger.Successf("Provider created") return namespacedName, nil } } return namespacedName, err } existing.Labels = provider.Labels existing.Spec = provider.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } provider = &existing logger.Successf("Provider updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "fmt" "os" "strings" "time" "github.com/spf13/cobra" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/transform" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) var createHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Create or update a HelmRelease resource", Long: `The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`, Example: ` # Create a HelmRelease with a chart from a HelmRepository source flux create hr podinfo \ --interval=10m \ --source=HelmRepository/podinfo \ --chart=podinfo \ --chart-version=">4.0.0" # Create a HelmRelease with a chart from a GitRepository source flux create hr podinfo \ --interval=10m \ --source=GitRepository/podinfo \ --chart=./charts/podinfo # Create a HelmRelease with a chart from a Bucket source flux create hr podinfo \ --interval=10m \ --source=Bucket/podinfo \ --chart=./charts/podinfo # Create a HelmRelease with values from local YAML files flux create hr podinfo \ --source=HelmRepository/podinfo \ --chart=podinfo \ --values=./my-values1.yaml \ --values=./my-values2.yaml # Create a HelmRelease with values from a Kubernetes secret kubectl -n app create secret generic my-secret-values \ --from-file=values.yaml=/path/to/my-secret-values.yaml flux -n app create hr podinfo \ --source=HelmRepository/podinfo \ --chart=podinfo \ --values-from=Secret/my-secret-values # Create a HelmRelease with a custom release name flux create hr podinfo \ --release-name=podinfo-dev \ --source=HelmRepository/podinfo \ --chart=podinfo # Create a HelmRelease targeting another namespace than the resource flux create hr podinfo \ --target-namespace=test \ --create-target-namespace=true \ --source=HelmRepository/podinfo \ --chart=podinfo # Create a HelmRelease with custom storage namespace for hub-and-spoke model flux create hr podinfo \ --target-namespace=production \ --storage-namespace=fluxcd-system \ --source=HelmRepository/podinfo \ --chart=podinfo # Create a HelmRelease using a source from a different namespace flux create hr podinfo \ --namespace=default \ --source=HelmRepository/podinfo.flux-system \ --chart=podinfo # Create a HelmRelease definition on disk without applying it on the cluster flux create hr podinfo \ --source=HelmRepository/podinfo \ --chart=podinfo \ --values=./values.yaml \ --export > podinfo-release.yaml # Create a HelmRelease using a chart from a HelmChart resource flux create hr podinfo \ --namespace=default \ --chart-ref=HelmChart/podinfo.flux-system \ # Create a HelmRelease using a chart from an OCIRepository resource flux create hr podinfo \ --namespace=default \ --chart-ref=OCIRepository/podinfo.flux-system`, RunE: createHelmReleaseCmdRun, } type helmReleaseFlags struct { name string source flags.HelmChartSource dependsOn []string chart string chartVersion string chartRef string targetNamespace string storageNamespace string createNamespace bool valuesFiles []string valuesFrom []string saName string crds flags.CRDsPolicy reconcileStrategy string chartInterval time.Duration kubeConfigSecretRef string } var helmReleaseArgs helmReleaseFlags var supportedHelmReleaseValuesFromKinds = []string{"Secret", "ConfigMap"} var supportedHelmReleaseReferenceKinds = []string{sourcev1.OCIRepositoryKind, sourcev1.HelmChartKind} func init() { createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[-]'") createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description()) createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chart, "chart", "", "Helm chart name or path") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)") createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.dependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed, supported formats '' and '/'") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.storageNamespace, "storage-namespace", "", "namespace to store the Helm release, defaults to the target namespace") createHelmReleaseCmd.Flags().BoolVar(&helmReleaseArgs.createNamespace, "create-target-namespace", false, "create the target namespace if it does not exist") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart created by the helm release(accepted values: Revision and ChartRevision)") createHelmReleaseCmd.Flags().DurationVarP(&helmReleaseArgs.chartInterval, "chart-interval", "", 0, "the interval of which to check for new chart versions") createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFiles, "values", nil, "local path to values.yaml files, also accepts comma-separated values") createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '/', where kind must be one of: (Secret,ConfigMap)") createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description()) createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartRef, "chart-ref", "", "the name of the HelmChart resource to use as source for the HelmRelease, in the format '/.', where kind must be one of: (OCIRepository,HelmChart)") createCmd.AddCommand(createHelmReleaseCmd) } func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if helmReleaseArgs.storageNamespace == "" && helmReleaseArgs.targetNamespace != "" { helmReleaseArgs.storageNamespace = helmReleaseArgs.targetNamespace } if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" { return fmt.Errorf("chart or chart-ref is required") } if helmReleaseArgs.chart != "" && helmReleaseArgs.chartRef != "" { return fmt.Errorf("cannot use --chart in combination with --chart-ref") } sourceLabels, err := parseLabels() if err != nil { return err } if !createArgs.export { logger.Generatef("generating HelmRelease") } if !validateStrategy(helmReleaseArgs.reconcileStrategy) { return fmt.Errorf("'%s' is an invalid reconcile strategy(valid: Revision, ChartVersion)", helmReleaseArgs.reconcileStrategy) } helmRelease := helmv2.HelmRelease{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: helmv2.HelmReleaseSpec{ ReleaseName: helmReleaseArgs.name, Interval: metav1.Duration{ Duration: createArgs.interval, }, TargetNamespace: helmReleaseArgs.targetNamespace, StorageNamespace: helmReleaseArgs.storageNamespace, Suspend: false, }, } if len(helmReleaseArgs.dependsOn) > 0 { ls := utils.MakeDependsOn(helmReleaseArgs.dependsOn) hrDependsOn := make([]helmv2.DependencyReference, 0, len(ls)) for _, d := range ls { hrDependsOn = append(hrDependsOn, helmv2.DependencyReference{ Name: d.Name, Namespace: d.Namespace, }) } helmRelease.Spec.DependsOn = hrDependsOn } switch { case helmReleaseArgs.chart != "": helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{ Spec: helmv2.HelmChartTemplateSpec{ Chart: helmReleaseArgs.chart, Version: helmReleaseArgs.chartVersion, SourceRef: helmv2.CrossNamespaceObjectReference{ Kind: helmReleaseArgs.source.Kind, Name: helmReleaseArgs.source.Name, Namespace: helmReleaseArgs.source.Namespace, }, ReconcileStrategy: helmReleaseArgs.reconcileStrategy, }, } if helmReleaseArgs.chartInterval != 0 { helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{ Duration: helmReleaseArgs.chartInterval, } } case helmReleaseArgs.chartRef != "": kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef) if kind != sourcev1.HelmChartKind && kind != sourcev1.OCIRepositoryKind { return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s", kind, strings.Join(supportedHelmReleaseReferenceKinds, ", ")) } helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ Kind: kind, Name: name, Namespace: ns, } } if helmReleaseArgs.kubeConfigSecretRef != "" { helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{ SecretRef: &meta.SecretKeyReference{ Name: helmReleaseArgs.kubeConfigSecretRef, }, } } if helmReleaseArgs.createNamespace { if helmRelease.Spec.Install == nil { helmRelease.Spec.Install = &helmv2.Install{} } helmRelease.Spec.Install.CreateNamespace = helmReleaseArgs.createNamespace } if helmReleaseArgs.saName != "" { helmRelease.Spec.ServiceAccountName = helmReleaseArgs.saName } if helmReleaseArgs.crds != "" { if helmRelease.Spec.Install == nil { helmRelease.Spec.Install = &helmv2.Install{} } helmRelease.Spec.Install.CRDs = helmv2.Create helmRelease.Spec.Upgrade = &helmv2.Upgrade{CRDs: helmv2.CRDsPolicy(helmReleaseArgs.crds.String())} } if len(helmReleaseArgs.valuesFiles) > 0 { valuesMap := make(map[string]interface{}) for _, v := range helmReleaseArgs.valuesFiles { data, err := os.ReadFile(v) if err != nil { return fmt.Errorf("reading values from %s failed: %w", v, err) } jsonBytes, err := yaml.YAMLToJSON(data) if err != nil { return fmt.Errorf("converting values to JSON from %s failed: %w", v, err) } jsonMap := make(map[string]interface{}) if err := json.Unmarshal(jsonBytes, &jsonMap); err != nil { return fmt.Errorf("unmarshaling values from %s failed: %w", v, err) } valuesMap = transform.MergeMaps(valuesMap, jsonMap) } jsonRaw, err := json.Marshal(valuesMap) if err != nil { return fmt.Errorf("marshaling values failed: %w", err) } helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: jsonRaw} } if len(helmReleaseArgs.valuesFrom) != 0 { values := []helmv2.ValuesReference{} for _, value := range helmReleaseArgs.valuesFrom { sourceKind, sourceName := utils.ParseObjectKindName(value) if sourceKind == "" { return fmt.Errorf("invalid Kubernetes object reference '%s', must be in format /", value) } cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmReleaseValuesFromKinds, sourceKind) if !ok { return fmt.Errorf("reference kind '%s' is not supported, must be one of: %s", sourceKind, strings.Join(supportedHelmReleaseValuesFromKinds, ", ")) } values = append(values, helmv2.ValuesReference{ Name: sourceName, Kind: cleanSourceKind, }) } helmRelease.Spec.ValuesFrom = values } if createArgs.export { return printExport(exportHelmRelease(&helmRelease)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying HelmRelease") namespacedName, err := upsertHelmRelease(ctx, kubeClient, &helmRelease) if err != nil { return err } logger.Waitingf("waiting for HelmRelease reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, &helmRelease)); err != nil { return err } logger.Successf("HelmRelease %s is ready", name) logger.Successf("applied revision %s", getHelmReleaseRevision(helmRelease)) return nil } func upsertHelmRelease(ctx context.Context, kubeClient client.Client, helmRelease *helmv2.HelmRelease) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: helmRelease.GetNamespace(), Name: helmRelease.GetName(), } var existing helmv2.HelmRelease err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, helmRelease); err != nil { return namespacedName, err } else { logger.Successf("HelmRelease created") return namespacedName, nil } } return namespacedName, err } existing.Labels = helmRelease.Labels existing.Spec = helmRelease.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } helmRelease = &existing logger.Successf("HelmRelease updated") return namespacedName, nil } func validateStrategy(input string) bool { allowedStrategy := []string{"Revision", "ChartVersion"} for _, strategy := range allowedStrategy { if strategy == input { return true } } return false } ================================================ FILE: cmd/flux/create_helmrelease_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "testing" func TestCreateHelmRelease(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setupHRSource(t, tmpl) tests := []struct { name string args string assert assertFunc }{ { name: "missing name", args: "create helmrelease --export", assert: assertError("name is required"), }, { name: "missing chart template and chartRef", args: "create helmrelease podinfo --export", assert: assertError("chart or chart-ref is required"), }, { name: "chart and chartRef used in combination", args: "create helmrelease podinfo --chart podinfo --chart-ref foobar/podinfo --export", assert: assertError("cannot use --chart in combination with --chart-ref"), }, { name: "unknown source kind", args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export", assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`), }, { name: "unknown chart reference kind", args: "create helmrelease podinfo --chart-ref foobar/podinfo --export", assert: assertError(`chart reference kind 'foobar' is not supported, must be one of: OCIRepository, HelmChart`), }, { name: "basic helmrelease", args: "create helmrelease podinfo --source Helmrepository/podinfo --chart podinfo --interval=1m0s --export", assert: assertGoldenTemplateFile("testdata/create_hr/basic.yaml", tmpl), }, { name: "chart with OCIRepository source", args: "create helmrelease podinfo --chart-ref OCIRepository/podinfo --interval=1m0s --export", assert: assertGoldenTemplateFile("testdata/create_hr/or_basic.yaml", tmpl), }, { name: "chart with HelmChart source", args: "create helmrelease podinfo --chart-ref HelmChart/podinfo --interval=1m0s --export", assert: assertGoldenTemplateFile("testdata/create_hr/hc_basic.yaml", tmpl), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: tt.assert, } cmd.runTestCmd(t) }) } } func setupHRSource(t *testing.T, tmpl map[string]string) { t.Helper() testEnv.CreateObjectFile("./testdata/create_hr/setup-source.yaml", tmpl, t) } ================================================ FILE: cmd/flux/create_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var createImageCmd = &cobra.Command{ Use: "image", Short: "Create or update resources dealing with image automation", Long: `The create image sub-commands work with image automation objects; that is, object controlling updates to git based on e.g., new container images being available.`, } func init() { createCmd.AddCommand(createImageCmd) } ================================================ FILE: cmd/flux/create_image_policy.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "regexp/syntax" "strings" "time" "unicode" "unicode/utf8" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fluxcd/pkg/apis/meta" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var createImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Create or update an ImagePolicy object", Long: `The create image policy command generates an ImagePolicy resource. An ImagePolicy object calculates a "latest image" given an image repository and a policy, e.g., semver. The image that sorts highest according to the policy is recorded in the status of the object.`, Example: ` # Create an ImagePolicy to select the latest stable release flux create image policy podinfo \ --image-ref=podinfo \ --select-semver=">=1.0.0" # Create an ImagePolicy to select the latest main branch build tagged as "${GIT_BRANCH}-${GIT_SHA:0:7}-$(date +%s)" flux create image policy podinfo \ --image-ref=podinfo \ --select-numeric=asc \ --filter-regex='^main-[a-f0-9]+-(?P[0-9]+)' \ --filter-extract='$ts'`, RunE: createImagePolicyRun} type imagePolicyFlags struct { imageRef string semver string alpha string numeric string filterRegex string filterExtract string reflectDigest string interval time.Duration } var imagePolicyArgs = imagePolicyFlags{} func init() { flags := createImagePolicyCmd.Flags() flags.StringVar(&imagePolicyArgs.imageRef, "image-ref", "", "the name of an image repository object") flags.StringVar(&imagePolicyArgs.semver, "select-semver", "", "a semver range to apply to tags; e.g., '1.x'") flags.StringVar(&imagePolicyArgs.alpha, "select-alpha", "", "use alphabetical sorting to select image; either \"asc\" meaning select the last, or \"desc\" meaning select the first") flags.StringVar(&imagePolicyArgs.numeric, "select-numeric", "", "use numeric sorting to select image; either \"asc\" meaning select the last, or \"desc\" meaning select the first") flags.StringVar(&imagePolicyArgs.filterRegex, "filter-regex", "", "regular expression pattern used to filter the image tags") flags.StringVar(&imagePolicyArgs.filterExtract, "filter-extract", "", "replacement pattern (using capture groups from --filter-regex) to use for sorting") flags.StringVar(&imagePolicyArgs.reflectDigest, "reflect-digest", "", "the digest reflection policy to use when observing latest image tags (one of 'Never', 'IfNotPresent', 'Never')") flags.DurationVar(&imagePolicyArgs.interval, "interval", 0, "the interval at which to check for new image digests when the policy is set to 'Always'") createImageCmd.AddCommand(createImagePolicyCmd) } func createImagePolicyRun(cmd *cobra.Command, args []string) error { objectName := args[0] if imagePolicyArgs.imageRef == "" { return fmt.Errorf("the name of an ImageRepository in the namespace is required (--image-ref)") } labels, err := parseLabels() if err != nil { return err } var policy = imagev1.ImagePolicy{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, Namespace: *kubeconfigArgs.Namespace, Labels: labels, }, Spec: imagev1.ImagePolicySpec{ ImageRepositoryRef: meta.NamespacedObjectReference{ Name: imagePolicyArgs.imageRef, }, }, } switch { case imagePolicyArgs.semver != "" && imagePolicyArgs.alpha != "": case imagePolicyArgs.semver != "" && imagePolicyArgs.numeric != "": case imagePolicyArgs.alpha != "" && imagePolicyArgs.numeric != "": return fmt.Errorf("only one of --select-semver, --select-alpha or --select-numeric can be specified") case imagePolicyArgs.semver != "": policy.Spec.Policy.SemVer = &imagev1.SemVerPolicy{ Range: imagePolicyArgs.semver, } case imagePolicyArgs.alpha != "": if imagePolicyArgs.alpha != "desc" && imagePolicyArgs.alpha != "asc" { return fmt.Errorf("--select-alpha must be one of [\"asc\", \"desc\"]") } policy.Spec.Policy.Alphabetical = &imagev1.AlphabeticalPolicy{ Order: imagePolicyArgs.alpha, } case imagePolicyArgs.numeric != "": if imagePolicyArgs.numeric != "desc" && imagePolicyArgs.numeric != "asc" { return fmt.Errorf("--select-numeric must be one of [\"asc\", \"desc\"]") } policy.Spec.Policy.Numerical = &imagev1.NumericalPolicy{ Order: imagePolicyArgs.numeric, } default: return fmt.Errorf("a policy must be provided with either --select-semver or --select-alpha") } if imagePolicyArgs.filterRegex != "" { exp, err := syntax.Parse(imagePolicyArgs.filterRegex, syntax.Perl) if err != nil { return fmt.Errorf("--filter-regex is an invalid regex pattern") } policy.Spec.FilterTags = &imagev1.TagFilter{ Pattern: imagePolicyArgs.filterRegex, } if imagePolicyArgs.filterExtract != "" { if err := validateExtractStr(imagePolicyArgs.filterExtract, exp.CapNames()); err != nil { return err } policy.Spec.FilterTags.Extract = imagePolicyArgs.filterExtract } } else if imagePolicyArgs.filterExtract != "" { return fmt.Errorf("cannot specify --filter-extract without specifying --filter-regex") } if p := imagev1.ReflectionPolicy(imagePolicyArgs.reflectDigest); p != "" { if p != imagev1.ReflectNever && p != imagev1.ReflectIfNotPresent && p != imagev1.ReflectAlways { return fmt.Errorf("invalid value for --reflect-digest, must be one of 'Never', 'IfNotPresent', 'Always'") } policy.Spec.DigestReflectionPolicy = p } if imagePolicyArgs.interval != 0 { if imagePolicyArgs.reflectDigest != string(imagev1.ReflectAlways) { return fmt.Errorf("the --interval flag can only be used with the 'Always' digest reflection policy, use --reflect-digest=Always") } policy.Spec.Interval = &metav1.Duration{Duration: imagePolicyArgs.interval} } if createArgs.export { return printExport(exportImagePolicy(&policy)) } var existing imagev1.ImagePolicy copyName(&existing, &policy) err = imagePolicyType.upsertAndWait(imagePolicyAdapter{&existing}, func() error { existing.Spec = policy.Spec existing.SetLabels(policy.Labels) return nil }) return err } // Performs a dry-run of the extract function in Regexp to validate the template func validateExtractStr(template string, capNames []string) error { for len(template) > 0 { i := strings.Index(template, "$") if i < 0 { return nil } template = template[i:] if len(template) > 1 && template[1] == '$' { template = template[2:] continue } name, num, rest, ok := extract(template) if !ok { // Malformed extract string, assume user didn't want this return fmt.Errorf("--filter-extract is malformed") } template = rest if num >= 0 { // we won't worry about numbers as we can't validate these continue } else { found := false for _, capName := range capNames { if name == capName { found = true } } if !found { return fmt.Errorf("capture group $%s used in --filter-extract not found in --filter-regex", name) } } } return nil } // extract method from the regexp package // returns the name or number of the value prepended by $ func extract(str string) (name string, num int, rest string, ok bool) { if len(str) < 2 || str[0] != '$' { return } brace := false if str[1] == '{' { brace = true str = str[2:] } else { str = str[1:] } i := 0 for i < len(str) { rune, size := utf8.DecodeRuneInString(str[i:]) if !unicode.IsLetter(rune) && !unicode.IsDigit(rune) && rune != '_' { break } i += size } if i == 0 { // empty name is not okay return } name = str[:i] if brace { if i >= len(str) || str[i] != '}' { // missing closing brace return } i++ } // Parse number. num = 0 for i := 0; i < len(name); i++ { if name[i] < '0' || '9' < name[i] || num >= 1e8 { num = -1 break } num = num*10 + int(name[i]) - '0' } // Disallow leading zeros. if name[0] == '0' && len(name) > 1 { num = -1 } rest = str[i:] ok = true return } ================================================ FILE: cmd/flux/create_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "time" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fluxcd/pkg/apis/meta" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var createImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Create or update an ImageRepository object", Long: `The create image repository command generates an ImageRepository resource. An ImageRepository object specifies an image repository to scan.`, Example: ` # Create an ImageRepository object to scan the alpine image repository: flux create image repository alpine-repo --image alpine --interval 20m # Create an image repository that uses an image pull secret (assumed to # have been created already): flux create image repository myapp-repo \ --secret-ref image-pull \ --image ghcr.io/example.com/myapp --interval 5m # Create a TLS secret for a local image registry using a self-signed # host certificate, and use it to scan an image. ca.pem is a file # containing the CA certificate used to sign the host certificate. flux create secret tls local-registry-cert --ca-file ./ca.pem flux create image repository app-repo \ --cert-secret-ref local-registry-cert \ --image local-registry:5000/app --interval 5m # Create a TLS secret with a client certificate and key, and use it # to scan a private image registry. flux create secret tls client-cert \ --cert-file client.crt --key-file client.key flux create image repository app-repo \ --cert-secret-ref client-cert \ --image registry.example.com/private/app --interval 5m`, RunE: createImageRepositoryRun, } type imageRepoFlags struct { image string secretRef string certSecretRef string timeout time.Duration } var imageRepoArgs = imageRepoFlags{} func init() { flags := createImageRepositoryCmd.Flags() flags.StringVar(&imageRepoArgs.image, "image", "", "the image repository to scan; e.g., library/alpine") flags.StringVar(&imageRepoArgs.secretRef, "secret-ref", "", "the name of a docker-registry secret to use for credentials") flags.StringVar(&imageRepoArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates") // NB there is already a --timeout in the global flags, for // controlling timeout on operations while e.g., creating objects. flags.DurationVar(&imageRepoArgs.timeout, "scan-timeout", 0, "a timeout for scanning; this defaults to the interval if not set") createImageCmd.AddCommand(createImageRepositoryCmd) } func createImageRepositoryRun(cmd *cobra.Command, args []string) error { objectName := args[0] if imageRepoArgs.image == "" { return fmt.Errorf("an image repository (--image) is required") } if _, err := name.NewRepository(imageRepoArgs.image); err != nil { return fmt.Errorf("unable to parse image value: %w", err) } labels, err := parseLabels() if err != nil { return err } var repo = imagev1.ImageRepository{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, Namespace: *kubeconfigArgs.Namespace, Labels: labels, }, Spec: imagev1.ImageRepositorySpec{ Image: imageRepoArgs.image, Interval: metav1.Duration{Duration: createArgs.interval}, }, } if imageRepoArgs.timeout != 0 { repo.Spec.Timeout = &metav1.Duration{Duration: imageRepoArgs.timeout} } if imageRepoArgs.secretRef != "" { repo.Spec.SecretRef = &meta.LocalObjectReference{ Name: imageRepoArgs.secretRef, } } if imageRepoArgs.certSecretRef != "" { repo.Spec.CertSecretRef = &meta.LocalObjectReference{ Name: imageRepoArgs.certSecretRef, } } if createArgs.export { return printExport(exportImageRepository(&repo)) } // a temp value for use with the rest var existing imagev1.ImageRepository copyName(&existing, &repo) err = imageRepositoryType.upsertAndWait(imageRepositoryAdapter{&existing}, func() error { existing.Spec = repo.Spec existing.Labels = repo.Labels return nil }) return err } ================================================ FILE: cmd/flux/create_image_update.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" autov1 "github.com/fluxcd/image-automation-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var createImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Create or update an ImageUpdateAutomation object", Long: `The create image update command generates an ImageUpdateAutomation resource. An ImageUpdateAutomation object specifies an automated update to images mentioned in YAMLs in a git repository.`, Example: ` # Configure image updates for the main repository created by flux bootstrap flux create image update flux-system \ --git-repo-ref=flux-system \ --git-repo-path="./clusters/my-cluster" \ --checkout-branch=main \ --author-name=flux \ --author-email=flux@example.com \ --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" # Configure image updates to push changes to a different branch, if the branch doesn't exists it will be created flux create image update flux-system \ --git-repo-ref=flux-system \ --git-repo-path="./clusters/my-cluster" \ --checkout-branch=main \ --push-branch=image-updates \ --author-name=flux \ --author-email=flux@example.com \ --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" # Configure image updates for a Git repository in a different namespace flux create image update apps \ --namespace=apps \ --git-repo-ref=flux-system \ --git-repo-namespace=flux-system \ --git-repo-path="./clusters/my-cluster" \ --checkout-branch=main \ --push-branch=image-updates \ --author-name=flux \ --author-email=flux@example.com \ --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" `, RunE: createImageUpdateRun, } type imageUpdateFlags struct { gitRepoName string gitRepoNamespace string gitRepoPath string checkoutBranch string pushBranch string commitTemplate string authorName string authorEmail string } var imageUpdateArgs = imageUpdateFlags{} func init() { flags := createImageUpdateCmd.Flags() flags.StringVar(&imageUpdateArgs.gitRepoName, "git-repo-ref", "", "the name of a GitRepository resource with details of the upstream Git repository") flags.StringVar(&imageUpdateArgs.gitRepoNamespace, "git-repo-namespace", "", "the namespace of the GitRepository resource, defaults to the ImageUpdateAutomation namespace") flags.StringVar(&imageUpdateArgs.gitRepoPath, "git-repo-path", "", "path to the directory containing the manifests to be updated, defaults to the repository root") flags.StringVar(&imageUpdateArgs.checkoutBranch, "checkout-branch", "", "the branch to checkout") flags.StringVar(&imageUpdateArgs.pushBranch, "push-branch", "", "the branch to push commits to, defaults to the checkout branch if not specified") flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages") flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author") flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author") createImageCmd.AddCommand(createImageUpdateCmd) } func createImageUpdateRun(cmd *cobra.Command, args []string) error { objectName := args[0] if imageUpdateArgs.gitRepoName == "" { return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)") } if imageUpdateArgs.checkoutBranch == "" { return fmt.Errorf("the Git repository branch is required (--checkout-branch)") } if imageUpdateArgs.authorName == "" { return fmt.Errorf("the author name is required (--author-name)") } if imageUpdateArgs.authorEmail == "" { return fmt.Errorf("the author email is required (--author-email)") } labels, err := parseLabels() if err != nil { return err } var update = autov1.ImageUpdateAutomation{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, Namespace: *kubeconfigArgs.Namespace, Labels: labels, }, Spec: autov1.ImageUpdateAutomationSpec{ SourceRef: autov1.CrossNamespaceSourceReference{ Kind: sourcev1.GitRepositoryKind, Name: imageUpdateArgs.gitRepoName, Namespace: imageUpdateArgs.gitRepoNamespace, }, GitSpec: &autov1.GitSpec{ Checkout: &autov1.GitCheckoutSpec{ Reference: sourcev1.GitRepositoryRef{ Branch: imageUpdateArgs.checkoutBranch, }, }, Commit: autov1.CommitSpec{ Author: autov1.CommitUser{ Name: imageUpdateArgs.authorName, Email: imageUpdateArgs.authorEmail, }, MessageTemplate: imageUpdateArgs.commitTemplate, }, }, Interval: metav1.Duration{ Duration: createArgs.interval, }, }, } if imageUpdateArgs.pushBranch != "" { update.Spec.GitSpec.Push = &autov1.PushSpec{ Branch: imageUpdateArgs.pushBranch, } } if imageUpdateArgs.gitRepoPath != "" { update.Spec.Update = &autov1.UpdateStrategy{ Path: imageUpdateArgs.gitRepoPath, Strategy: autov1.UpdateStrategySetters, } } if createArgs.export { return printExport(exportImageUpdate(&update)) } var existing autov1.ImageUpdateAutomation copyName(&existing, &update) err = imageUpdateAutomationType.upsertAndWait(imageUpdateAutomationAdapter{&existing}, func() error { existing.Spec = update.Spec existing.Labels = update.Labels return nil }) return err } ================================================ FILE: cmd/flux/create_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "strings" "time" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) var createKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Create or update a Kustomization resource", Long: `The create command generates a Kustomization resource for a given source.`, Example: ` # Create a Kustomization resource from a source at a given path flux create kustomization kyverno \ --source=GitRepository/kyverno \ --path="./config/release" \ --prune=true \ --interval=60m \ --wait=true \ --health-check-timeout=3m # Create a Kustomization resource that depends on the previous one flux create kustomization kyverno-policies \ --depends-on=kyverno \ --source=GitRepository/kyverno-policies \ --path="./policies/flux" \ --prune=true \ --interval=5m # Create a Kustomization using a source from a different namespace flux create kustomization podinfo \ --namespace=default \ --source=GitRepository/podinfo.flux-system \ --path="./kustomize" \ --prune=true \ --interval=5m # Create a Kustomization resource that references an OCIRepository flux create kustomization podinfo \ --source=OCIRepository/podinfo \ --target-namespace=default \ --prune=true \ --interval=5m # Create a Kustomization resource that references a Bucket flux create kustomization secrets \ --source=Bucket/secrets \ --prune=true \ --interval=5m`, RunE: createKsCmdRun, } type kustomizationFlags struct { source flags.KustomizationSource path flags.SafeRelativePath prune bool dependsOn []string validation string healthCheck []string healthTimeout time.Duration saName string decryptionProvider flags.DecryptionProvider decryptionSecret string targetNamespace string wait bool kubeConfigSecretRef string retryInterval time.Duration } var kustomizationArgs = NewKustomizationFlags() func init() { createKsCmd.Flags().Var(&kustomizationArgs.source, "source", kustomizationArgs.source.Description()) createKsCmd.Flags().Var(&kustomizationArgs.path, "path", "path to the directory containing a kustomization.yaml file") createKsCmd.Flags().BoolVar(&kustomizationArgs.prune, "prune", false, "enable garbage collection") createKsCmd.Flags().BoolVar(&kustomizationArgs.wait, "wait", false, "enable health checking of all the applied resources") createKsCmd.Flags().StringSliceVar(&kustomizationArgs.healthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '/.'") createKsCmd.Flags().DurationVar(&kustomizationArgs.healthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations") createKsCmd.Flags().StringVar(&kustomizationArgs.validation, "validation", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'") createKsCmd.Flags().StringSliceVar(&kustomizationArgs.dependsOn, "depends-on", nil, "Kustomization that must be ready before this Kustomization can be applied, supported formats '' and '/', also accepts comma-separated values") createKsCmd.Flags().StringVar(&kustomizationArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this Kustomization") createKsCmd.Flags().Var(&kustomizationArgs.decryptionProvider, "decryption-provider", kustomizationArgs.decryptionProvider.Description()) createKsCmd.Flags().StringVar(&kustomizationArgs.decryptionSecret, "decryption-secret", "", "set the Kubernetes secret name that contains the OpenPGP private keys used for sops decryption") createKsCmd.Flags().StringVar(&kustomizationArgs.targetNamespace, "target-namespace", "", "overrides the namespace of all Kustomization objects reconciled by this Kustomization") createKsCmd.Flags().StringVar(&kustomizationArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster") createKsCmd.Flags().MarkDeprecated("validation", "this arg is no longer used, all resources are validated using server-side apply dry-run") createKsCmd.Flags().DurationVar(&kustomizationArgs.retryInterval, "retry-interval", 0, "the interval at which to retry a previously failed reconciliation") createCmd.AddCommand(createKsCmd) } func NewKustomizationFlags() kustomizationFlags { return kustomizationFlags{ path: "./", } } func createKsCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if kustomizationArgs.path == "" { return fmt.Errorf("path is required") } if !strings.HasPrefix(kustomizationArgs.path.String(), "./") { return fmt.Errorf("path must begin with ./") } if !createArgs.export { logger.Generatef("generating Kustomization") } kslabels, err := parseLabels() if err != nil { return err } kustomization := kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: kslabels, }, Spec: kustomizev1.KustomizationSpec{ Interval: metav1.Duration{ Duration: createArgs.interval, }, Path: kustomizationArgs.path.ToSlash(), Prune: kustomizationArgs.prune, SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: kustomizationArgs.source.Kind, Name: kustomizationArgs.source.Name, Namespace: kustomizationArgs.source.Namespace, }, Suspend: false, TargetNamespace: kustomizationArgs.targetNamespace, }, } if len(kustomizationArgs.dependsOn) > 0 { ls := utils.MakeDependsOn(kustomizationArgs.dependsOn) ksDependsOn := make([]kustomizev1.DependencyReference, 0, len(ls)) for _, d := range ls { ksDependsOn = append(ksDependsOn, kustomizev1.DependencyReference{ Name: d.Name, Namespace: d.Namespace, }) } kustomization.Spec.DependsOn = ksDependsOn } if kustomizationArgs.kubeConfigSecretRef != "" { kustomization.Spec.KubeConfig = &meta.KubeConfigReference{ SecretRef: &meta.SecretKeyReference{ Name: kustomizationArgs.kubeConfigSecretRef, }, } } if len(kustomizationArgs.healthCheck) > 0 && !kustomizationArgs.wait { healthChecks := make([]meta.NamespacedObjectKindReference, 0) for _, w := range kustomizationArgs.healthCheck { kindObj := strings.Split(w, "/") if len(kindObj) != 2 { return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj) } kind := kindObj[0] //TODO: (stefan) extend this list with all the kstatus builtin kinds kinds := map[string]bool{ "Deployment": true, "DaemonSet": true, "StatefulSet": true, helmv2.HelmReleaseKind: true, } if !kinds[kind] { return fmt.Errorf("invalid health check kind '%s' can be HelmRelease, Deployment, DaemonSet or StatefulSet", kind) } nameNs := strings.Split(kindObj[1], ".") if len(nameNs) != 2 { return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w) } check := meta.NamespacedObjectKindReference{ Kind: kind, Name: nameNs[0], Namespace: nameNs[1], } if kind == helmv2.HelmReleaseKind { check.APIVersion = helmv2.GroupVersion.String() } healthChecks = append(healthChecks, check) } kustomization.Spec.HealthChecks = healthChecks kustomization.Spec.Timeout = &metav1.Duration{ Duration: kustomizationArgs.healthTimeout, } } if kustomizationArgs.wait { kustomization.Spec.Wait = true kustomization.Spec.Timeout = &metav1.Duration{ Duration: kustomizationArgs.healthTimeout, } } if kustomizationArgs.saName != "" { kustomization.Spec.ServiceAccountName = kustomizationArgs.saName } if kustomizationArgs.decryptionProvider != "" { kustomization.Spec.Decryption = &kustomizev1.Decryption{ Provider: kustomizationArgs.decryptionProvider.String(), } if kustomizationArgs.decryptionSecret != "" { kustomization.Spec.Decryption.SecretRef = &meta.LocalObjectReference{Name: kustomizationArgs.decryptionSecret} } } if kustomizationArgs.retryInterval > 0 { kustomization.Spec.RetryInterval = &metav1.Duration{Duration: kustomizationArgs.retryInterval} } if createArgs.export { return printExport(exportKs(&kustomization)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying Kustomization") namespacedName, err := upsertKustomization(ctx, kubeClient, &kustomization) if err != nil { return err } logger.Waitingf("waiting for Kustomization reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, &kustomization)); err != nil { return err } logger.Successf("Kustomization %s is ready", name) logger.Successf("applied revision %s", kustomization.Status.LastAppliedRevision) return nil } func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomization *kustomizev1.Kustomization) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: kustomization.GetNamespace(), Name: kustomization.GetName(), } var existing kustomizev1.Kustomization err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, kustomization); err != nil { return namespacedName, err } else { logger.Successf("Kustomization created") return namespacedName, nil } } return namespacedName, err } existing.Labels = kustomization.Labels existing.Spec = kustomization.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } kustomization = &existing logger.Successf("Kustomization updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" ) var createReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Create or update a Receiver resource", Long: `The create receiver command generates a Receiver resource.`, Example: ` # Create a Receiver flux create receiver github-receiver \ --type github \ --event ping \ --event push \ --secret-ref webhook-token \ --resource GitRepository/webapp \ --resource HelmRepository/webapp`, RunE: createReceiverCmdRun, } type receiverFlags struct { receiverType string secretRef string events []string resources []string } var receiverArgs receiverFlags func init() { createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "") createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values") createCmd.AddCommand(createReceiverCmd) } func createReceiverCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if receiverArgs.receiverType == "" { return fmt.Errorf("Receiver type is required") } if receiverArgs.secretRef == "" { return fmt.Errorf("secret ref is required") } resources := []notificationv1.CrossNamespaceObjectReference{} for _, resource := range receiverArgs.resources { kind, name := utils.ParseObjectKindName(resource) if kind == "" { return fmt.Errorf("invalid event source '%s', must be in format /", resource) } resources = append(resources, notificationv1.CrossNamespaceObjectReference{ Kind: kind, Name: name, }) } if len(resources) == 0 { return fmt.Errorf("atleast one resource is required") } sourceLabels, err := parseLabels() if err != nil { return err } if !createArgs.export { logger.Generatef("generating Receiver") } receiver := notificationv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: notificationv1.ReceiverSpec{ Type: receiverArgs.receiverType, Events: receiverArgs.events, Resources: resources, SecretRef: meta.LocalObjectReference{ Name: receiverArgs.secretRef, }, Suspend: false, }, } if createArgs.export { return printExport(exportReceiver(&receiver)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying Receiver") namespacedName, err := upsertReceiver(ctx, kubeClient, &receiver) if err != nil { return err } logger.Waitingf("waiting for Receiver reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, &receiver)); err != nil { return err } logger.Successf("Receiver %s is ready", name) logger.Successf("generated webhook URL %s", receiver.Status.WebhookPath) return nil } func upsertReceiver(ctx context.Context, kubeClient client.Client, receiver *notificationv1.Receiver) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: receiver.GetNamespace(), Name: receiver.GetName(), } var existing notificationv1.Receiver err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, receiver); err != nil { return namespacedName, err } else { logger.Successf("Receiver created") return namespacedName, nil } } return namespacedName, err } existing.Labels = receiver.Labels existing.Spec = receiver.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } receiver = &existing logger.Successf("Receiver updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_secret.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) var createSecretCmd = &cobra.Command{ Use: "secret", Short: "Create or update Kubernetes secrets", Long: `The create source sub-commands generate Kubernetes secrets specific to Flux.`, } func init() { createCmd.AddCommand(createSecretCmd) } func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error { namespacedName := types.NamespacedName{ Namespace: secret.GetNamespace(), Name: secret.GetName(), } var existing corev1.Secret err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, &secret); err != nil { return err } else { return nil } } return err } existing.StringData = secret.StringData if err := kubeClient.Update(ctx, &existing); err != nil { return err } return nil } ================================================ FILE: cmd/flux/create_secret_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "crypto/elliptic" "fmt" "net/url" "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretGitCmd = &cobra.Command{ Use: "git [name]", Short: "Create or update a Kubernetes secret for Git authentication", Long: `The create secret git command generates a Kubernetes secret with Git credentials. For Git over SSH, the host and SSH keys are automatically generated and stored in the secret. For Git over HTTP/S, the provided basic authentication credentials or bearer authentication token are stored in the secret.`, Example: ` # Create a Git SSH authentication secret using an ECDSA P-521 curve public key flux create secret git podinfo-auth \ --url=ssh://git@github.com/stefanprodan/podinfo \ --ssh-key-algorithm=ecdsa \ --ssh-ecdsa-curve=p521 # Create a Git SSH authentication secret with a passwordless private key from file # The public SSH host key will still be gathered from the host flux create secret git podinfo-auth \ --url=ssh://git@github.com/stefanprodan/podinfo \ --private-key-file=./private.key # Create a Git SSH authentication secret with a passworded private key from file # The public SSH host key will still be gathered from the host flux create secret git podinfo-auth \ --url=ssh://git@github.com/stefanprodan/podinfo \ --private-key-file=./private.key \ --password= # Create a secret for a Git repository using basic authentication flux create secret git podinfo-auth \ --url=https://github.com/stefanprodan/podinfo \ --username=username \ --password=password # Create a Git SSH secret on disk flux create secret git podinfo-auth \ --url=ssh://git@github.com/stefanprodan/podinfo \ --export > podinfo-auth.yaml # Print the deploy key yq eval '.stringData."identity.pub"' podinfo-auth.yaml # Encrypt the secret on disk with Mozilla SOPS sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place podinfo-auth.yaml`, RunE: createSecretGitCmdRun, } type secretGitFlags struct { url string username string password string keyAlgorithm flags.PublicKeyAlgorithm rsaBits flags.RSAKeyBits ecdsaCurve flags.ECDSACurve caCrtFile string privateKeyFile string bearerToken string } var secretGitArgs = NewSecretGitFlags() func init() { createSecretGitCmd.Flags().StringVar(&secretGitArgs.url, "url", "", "git address, e.g. ssh://git@host/org/repository") createSecretGitCmd.Flags().StringVarP(&secretGitArgs.username, "username", "u", "", "basic authentication username") createSecretGitCmd.Flags().StringVarP(&secretGitArgs.password, "password", "p", "", "basic authentication password") createSecretGitCmd.Flags().Var(&secretGitArgs.keyAlgorithm, "ssh-key-algorithm", secretGitArgs.keyAlgorithm.Description()) createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description()) createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description()) createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates") createSecretGitCmd.Flags().StringVar(&secretGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") createSecretGitCmd.Flags().StringVar(&secretGitArgs.bearerToken, "bearer-token", "", "bearer authentication token") createSecretCmd.AddCommand(createSecretGitCmd) } func NewSecretGitFlags() secretGitFlags { return secretGitFlags{ keyAlgorithm: flags.PublicKeyAlgorithm(sourcesecret.ECDSAPrivateKeyAlgorithm), rsaBits: 2048, ecdsaCurve: flags.ECDSACurve{Curve: elliptic.P384()}, } } func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if secretGitArgs.url == "" { return fmt.Errorf("url is required") } u, err := url.Parse(secretGitArgs.url) if err != nil { return fmt.Errorf("git URL parse failed: %w", err) } labels, err := parseLabels() if err != nil { return err } opts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: labels, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } switch u.Scheme { case "ssh": keypair, err := sourcesecret.LoadKeyPairFromPath(secretGitArgs.privateKeyFile, secretGitArgs.password) if err != nil { return err } opts.Keypair = keypair opts.SSHHostname = u.Host opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm) opts.RSAKeyBits = int(secretGitArgs.rsaBits) opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve opts.Password = secretGitArgs.password case "http", "https": if (secretGitArgs.username == "" || secretGitArgs.password == "") && secretGitArgs.bearerToken == "" { return fmt.Errorf("for Git over HTTP/S the username and password, or a bearer token is required") } opts.Username = secretGitArgs.username opts.Password = secretGitArgs.password opts.BearerToken = secretGitArgs.bearerToken if secretGitArgs.username != "" && secretGitArgs.password != "" && secretGitArgs.bearerToken != "" { return fmt.Errorf("user credentials and bearer token cannot be used together") } // --ca-crt-file takes precedence over --ca-file. if secretGitArgs.caCrtFile != "" { opts.CACrt, err = os.ReadFile(secretGitArgs.caCrtFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } default: return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) } secret, err := sourcesecret.GenerateGit(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok { logger.Generatef("deploy key: %s", ppk) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("git secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_git_test.go ================================================ package main import ( "fmt" "os" "testing" ) func TestCreateGitSecret(t *testing.T) { file, err := os.CreateTemp(t.TempDir(), "ca-crt") if err != nil { t.Fatal("could not create CA certificate file") } _, err = file.Write([]byte("ca-data")) if err != nil { t.Fatal("could not write to CA certificate file") } tests := []struct { name string args string assert assertFunc }{ { name: "no args", args: "create secret git", assert: assertError("name is required"), }, { name: "basic secret", args: "create secret git podinfo-auth --url=https://github.com/stefanprodan/podinfo --username=my-username --password=my-password --namespace=my-namespace --export", assert: assertGoldenFile("./testdata/create_secret/git/secret-git-basic.yaml"), }, { name: "ssh key", args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa.private --namespace=my-namespace --export", assert: assertGoldenFile("testdata/create_secret/git/git-ssh-secret.yaml"), }, { name: "ssh key with password", args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa-password.private --password=password --namespace=my-namespace --export", assert: assertGoldenFile("testdata/create_secret/git/git-ssh-secret-password.yaml"), }, { name: "git authentication with bearer token", args: "create secret git bearer-token-auth --url=https://github.com/stefanprodan/podinfo --bearer-token=ghp_baR2qnFF0O41WlucePL3udt2N9vVZS4R0hAS --namespace=my-namespace --export", assert: assertGoldenFile("testdata/create_secret/git/git-bearer-token.yaml"), }, { name: "git authentication with CA certificate", args: fmt.Sprintf("create secret git ca-crt --url=https://github.com/stefanprodan/podinfo --password=my-password --username=my-username --ca-crt-file=%s --namespace=my-namespace --export", file.Name()), assert: assertGoldenFile("testdata/create_secret/git/secret-ca-crt.yaml"), }, { name: "git authentication with basic auth and bearer token", args: "create secret git podinfo-auth --url=https://github.com/stefanprodan/podinfo --username=aaa --password=zzzz --bearer-token=aaaa --namespace=my-namespace --export", assert: assertError("user credentials and bearer token cannot be used together"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_github_app.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" ) var createSecretGitHubAppCmd = &cobra.Command{ Use: "githubapp [name]", Short: "Create or update a github app secret", Long: withPreviewNote(`The create secret githubapp command generates a Kubernetes secret that can be used for GitRepository authentication with github app`), Example: ` # Create a githubapp authentication secret on disk and encrypt it with Mozilla SOPS flux create secret githubapp podinfo-auth \ --app-id="1" \ --app-installation-id="2" \ --app-private-key=./private-key-file.pem \ --export > githubapp-auth.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place githubapp-auth.yaml `, RunE: createSecretGitHubAppCmdRun, } type secretGitHubAppFlags struct { appID string appInstallationOwner string appInstallationID string privateKeyFile string baseURL string } var secretGitHubAppArgs = secretGitHubAppFlags{} func init() { createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationOwner, "app-installation-owner", "", "github app installation owner (user or organization)") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path") createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL") createSecretCmd.AddCommand(createSecretGitHubAppCmd) } func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("name is required") } secretName := args[0] privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile) if err != nil { return fmt.Errorf("unable to read private key file: %w", err) } opts := sourcesecret.Options{ Name: secretName, Namespace: *kubeconfigArgs.Namespace, GitHubAppID: secretGitHubAppArgs.appID, GitHubAppInstallationOwner: secretGitHubAppArgs.appInstallationOwner, GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID, GitHubAppPrivateKey: string(privateKey), GitHubAppBaseURL: secretGitHubAppArgs.baseURL, } secret, err := sourcesecret.GenerateGitHubApp(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("githubapp secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_githubapp_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateSecretGitHubApp(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { name: "create githubapp secret with missing name", args: "create secret githubapp", assert: assertError("name is required"), }, { name: "create githubapp secret with private key file that does not exist", args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem", assert: assertError("unable to read private key file: open pk.pem: no such file or directory"), }, { name: "create githubapp secret with app info", args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-owner my-org --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export", assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"), }, { name: "create githubapp secret with appinfo and base url", args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --app-base-url www.example.com/api/v3 --export", assert: assertGoldenFile("testdata/create_secret/githubapp/secret-with-baseurl.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_helm.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Create or update a Kubernetes secret for Helm repository authentication", Long: `The create secret helm command generates a Kubernetes secret with basic authentication credentials.`, Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS flux create secret helm repo-auth \ --namespace=my-namespace \ --username=my-username \ --password=my-password \ --export > repo-auth.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place repo-auth.yaml`, RunE: createSecretHelmCmdRun, } type secretHelmFlags struct { username string password string secretTLSFlags } var secretHelmArgs secretHelmFlags func init() { flags := createSecretHelmCmd.Flags() flags.StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username") flags.StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password") flags.StringVar(&secretHelmArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path") flags.StringVar(&secretHelmArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path") flags.StringVar(&secretHelmArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path") createSecretCmd.AddCommand(createSecretHelmCmd) } func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { name := args[0] labels, err := parseLabels() if err != nil { return err } caBundle := []byte{} if secretHelmArgs.caCrtFile != "" { var err error caBundle, err = os.ReadFile(secretHelmArgs.caCrtFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } var certFile, keyFile []byte if secretHelmArgs.tlsCrtFile != "" { if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil { return fmt.Errorf("failed to read cert file: %w", err) } } if secretHelmArgs.tlsKeyFile != "" { if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil { return fmt.Errorf("failed to read key file: %w", err) } } opts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: labels, Username: secretHelmArgs.username, Password: secretHelmArgs.password, CACrt: caBundle, TLSCrt: certFile, TLSKey: keyFile, } secret, err := sourcesecret.GenerateHelm(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("helm secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_helm_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateHelmSecret(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { args: "create secret helm", assert: assertError("name is required"), }, { args: "create secret helm helm-secret --username=my-username --password=my-password --namespace=my-namespace --export", assert: assertGoldenFile("testdata/create_secret/helm/secret-helm.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_notation.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/notaryproject/notation-go/verifier/trustpolicy" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" ) var createSecretNotationCmd = &cobra.Command{ Use: "notation [name]", Short: "Create or update a Kubernetes secret for verifications of artifacts signed by Notation", Long: withPreviewNote(`The create secret notation command generates a Kubernetes secret with root ca certificates and trust policy.`), Example: ` # Create a Notation configuration secret on disk and encrypt it with Mozilla SOPS flux create secret notation my-notation-cert \ --namespace=my-namespace \ --trust-policy-file=./my-trust-policy.json \ --ca-cert-file=./my-cert.crt \ --export > my-notation-cert.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place my-notation-cert.yaml`, RunE: createSecretNotationCmdRun, } type secretNotationFlags struct { trustPolicyFile string caCrtFile []string } var secretNotationArgs secretNotationFlags func init() { createSecretNotationCmd.Flags().StringVar(&secretNotationArgs.trustPolicyFile, "trust-policy-file", "", "notation trust policy file path") createSecretNotationCmd.Flags().StringSliceVar(&secretNotationArgs.caCrtFile, "ca-cert-file", []string{}, "root ca cert file path") createSecretCmd.AddCommand(createSecretNotationCmd) } func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("name is required") } if secretNotationArgs.caCrtFile == nil || len(secretNotationArgs.caCrtFile) == 0 { return fmt.Errorf("--ca-cert-file is required") } if secretNotationArgs.trustPolicyFile == "" { return fmt.Errorf("--trust-policy-file is required") } name := args[0] labels, err := parseLabels() if err != nil { return err } policy, err := os.ReadFile(secretNotationArgs.trustPolicyFile) if err != nil { return fmt.Errorf("unable to read trust policy file: %w", err) } var doc trustpolicy.Document if err := json.Unmarshal(policy, &doc); err != nil { return fmt.Errorf("failed to unmarshal trust policy %s: %w", secretNotationArgs.trustPolicyFile, err) } if err := doc.Validate(); err != nil { return fmt.Errorf("invalid trust policy: %w", err) } var ( caCerts []sourcesecret.VerificationCrt fileErr error ) for _, caCrtFile := range secretNotationArgs.caCrtFile { fileName := filepath.Base(caCrtFile) if !strings.HasSuffix(fileName, ".crt") && !strings.HasSuffix(fileName, ".pem") { fileErr = errors.Join(fileErr, fmt.Errorf("%s must end with either .crt or .pem", fileName)) continue } caBundle, err := os.ReadFile(caCrtFile) if err != nil { fileErr = errors.Join(fileErr, fmt.Errorf("unable to read TLS CA file: %w", err)) continue } caCerts = append(caCerts, sourcesecret.VerificationCrt{Name: fileName, CACrt: caBundle}) } if fileErr != nil { return fileErr } if len(caCerts) == 0 { return fmt.Errorf("no CA certs found") } opts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: labels, VerificationCrts: caCerts, TrustPolicy: policy, } secret, err := sourcesecret.GenerateNotation(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("notation configuration secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_notation_test.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "path/filepath" "testing" ) const ( trustPolicy = "./testdata/create_secret/notation/test-trust-policy.json" invalidTrustPolicy = "./testdata/create_secret/notation/invalid-trust-policy.json" invalidJson = "./testdata/create_secret/notation/invalid.json" testCertFolder = "./testdata/create_secret/notation" ) func TestCreateNotationSecret(t *testing.T) { crt, err := os.Create(filepath.Join(t.TempDir(), "ca.crt")) if err != nil { t.Fatal("could not create ca.crt file") } pem, err := os.Create(filepath.Join(t.TempDir(), "ca.pem")) if err != nil { t.Fatal("could not create ca.pem file") } invalidCert, err := os.Create(filepath.Join(t.TempDir(), "ca.p12")) if err != nil { t.Fatal("could not create ca.p12 file") } _, err = crt.Write([]byte("ca-data-crt")) if err != nil { t.Fatal("could not write to crt certificate file") } _, err = pem.Write([]byte("ca-data-pem")) if err != nil { t.Fatal("could not write to pem certificate file") } tests := []struct { name string args string assert assertFunc }{ { name: "no args", args: "create secret notation", assert: assertError("name is required"), }, { name: "no trust policy", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s", testCertFolder), assert: assertError("--trust-policy-file is required"), }, { name: "no cert", args: fmt.Sprintf("create secret notation notation-config --trust-policy-file=%s", trustPolicy), assert: assertError("--ca-cert-file is required"), }, { name: "non pem and crt cert", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", invalidCert.Name(), trustPolicy), assert: assertError("ca.p12 must end with either .crt or .pem"), }, { name: "invalid trust policy", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidTrustPolicy), assert: assertError("invalid trust policy: trust policy: a trust policy statement is missing a name, every statement requires a name"), }, { name: "invalid trust policy json", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidJson), assert: assertError(fmt.Sprintf("failed to unmarshal trust policy %s: json: cannot unmarshal string into Go value of type trustpolicy.Document", invalidJson)), }, { name: "crt secret", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), trustPolicy), assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-crt.yaml"), }, { name: "pem secret", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", pem.Name(), trustPolicy), assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-pem.yaml"), }, { name: "multi secret", args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), pem.Name(), trustPolicy), assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-multi.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer func() { secretNotationArgs = secretNotationFlags{} }() cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" ) var createSecretOCICmd = &cobra.Command{ Use: "oci [name]", Short: "Create or update a Kubernetes image pull secret", Long: withPreviewNote(`The create secret oci command generates a Kubernetes secret that can be used for OCIRepository authentication`), Example: ` # Create an OCI authentication secret on disk and encrypt it with Mozilla SOPS flux create secret oci podinfo-auth \ --url=ghcr.io \ --username=username \ --password=password \ --export > repo-auth.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place repo-auth.yaml `, RunE: createSecretOCICmdRun, } type secretOCIFlags struct { url string password string username string } var secretOCIArgs = secretOCIFlags{} func init() { createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts") createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username") createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication password") createSecretCmd.AddCommand(createSecretOCICmd) } func createSecretOCICmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("name is required") } secretName := args[0] if secretOCIArgs.url == "" { return fmt.Errorf("--url is required") } if secretOCIArgs.username == "" { return fmt.Errorf("--username is required") } if secretOCIArgs.password == "" { return fmt.Errorf("--password is required") } if _, err := name.ParseReference(secretOCIArgs.url); err != nil { return fmt.Errorf("error parsing url: '%s'", err) } opts := sourcesecret.Options{ Name: secretName, Namespace: *kubeconfigArgs.Namespace, Registry: secretOCIArgs.url, Password: secretOCIArgs.password, Username: secretOCIArgs.username, } secret, err := sourcesecret.GenerateOCI(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_oci_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateSecretOCI(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { args: "create secret oci", assert: assertError("name is required"), }, { args: "create secret oci ghcr", assert: assertError("--url is required"), }, { args: "create secret oci ghcr --namespace=my-namespace --url ghcr.io --username stefanprodan --password=password --export", assert: assertGoldenFile("testdata/create_secret/oci/create-secret.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_proxy.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "errors" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretProxyCmd = &cobra.Command{ Use: "proxy [name]", Short: "Create or update a Kubernetes secret for proxy authentication", Long: `The create secret proxy command generates a Kubernetes secret with the proxy address and the basic authentication credentials.`, Example: ` # Create a proxy secret on disk and encrypt it with SOPS flux create secret proxy my-proxy \ --namespace=my-namespace \ --address=https://my-proxy.com \ --username=my-username \ --password=my-password \ --export > proxy.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place proxy.yaml`, RunE: createSecretProxyCmdRun, } type secretProxyFlags struct { address string username string password string } var secretProxyArgs secretProxyFlags func init() { createSecretProxyCmd.Flags().StringVar(&secretProxyArgs.address, "address", "", "proxy address") createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.username, "username", "u", "", "basic authentication username") createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.password, "password", "p", "", "basic authentication password") createSecretCmd.AddCommand(createSecretProxyCmd) } func createSecretProxyCmdRun(cmd *cobra.Command, args []string) error { name := args[0] labels, err := parseLabels() if err != nil { return err } if secretProxyArgs.address == "" { return errors.New("address is required") } opts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: labels, Address: secretProxyArgs.address, Username: secretProxyArgs.username, Password: secretProxyArgs.password, } secret, err := sourcesecret.GenerateProxy(opts) if err != nil { return err } if createArgs.export { rootCmd.Println(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("proxy secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_proxy_test.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateProxySecret(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { args: "create secret proxy proxy-secret", assert: assertError("address is required"), }, { args: "create secret proxy proxy-secret --address=https://my-proxy.com --username=my-username --password=my-password --namespace=my-namespace --export", assert: assertGoldenFile("testdata/create_secret/proxy/secret-proxy.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_secret_tls.go ================================================ /* Copyright 2020, 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretTLSCmd = &cobra.Command{ Use: "tls [name]", Short: "Create or update a Kubernetes secret with TLS certificates", Long: `The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`, Example: ` # Create a TLS secret on disk and encrypt it with SOPS. # Files are expected to be PEM-encoded. flux create secret tls certs \ --namespace=my-namespace \ --tls-crt-file=./client.crt \ --tls-key-file=./client.key \ --ca-crt-file=./ca.crt \ --export > certs.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ --in-place certs.yaml`, RunE: createSecretTLSCmdRun, } type secretTLSFlags struct { caCrtFile string tlsKeyFile string tlsCrtFile string } var secretTLSArgs secretTLSFlags func init() { createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path") createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path") createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path") createSecretCmd.AddCommand(createSecretTLSCmd) } func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { name := args[0] labels, err := parseLabels() if err != nil { return err } opts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: labels, } if secretTLSArgs.caCrtFile != "" { opts.CACrt, err = os.ReadFile(secretTLSArgs.caCrtFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } if secretTLSArgs.tlsCrtFile != "" { if opts.TLSCrt, err = os.ReadFile(secretTLSArgs.tlsCrtFile); err != nil { return fmt.Errorf("failed to read cert file: %w", err) } } if secretTLSArgs.tlsKeyFile != "" { if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil { return fmt.Errorf("failed to read key file: %w", err) } } secret, err := sourcesecret.GenerateTLS(opts) if err != nil { return err } if createArgs.export { rootCmd.Print(secret.Content) return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var s corev1.Secret if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } logger.Actionf("tls secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } ================================================ FILE: cmd/flux/create_secret_tls_test.go ================================================ package main import ( "testing" ) func TestCreateTlsSecret(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { args: "create secret tls", assert: assertError("name is required"), }, { args: "create secret tls certs --namespace=my-namespace --tls-crt-file=./testdata/create_secret/tls/test-cert.pem --tls-key-file=./testdata/create_secret/tls/test-key.pem --ca-crt-file=./testdata/create_secret/tls/test-ca.pem --export", assert: assertGoldenFile("testdata/create_secret/tls/secret-tls.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "time" "github.com/spf13/cobra" ) var createSourceCmd = &cobra.Command{ Use: "source", Short: "Create or update sources", Long: `The create source sub-commands generate sources.`, } type createSourceFlags struct { fetchTimeout time.Duration } var createSourceArgs createSourceFlags func init() { createSourceCmd.PersistentFlags().DurationVar(&createSourceArgs.fetchTimeout, "fetch-timeout", createSourceArgs.fetchTimeout, "set a timeout for fetch operations performed by source-controller (e.g. 'git clone' or 'helm repo update')") createCmd.AddCommand(createSourceCmd) } ================================================ FILE: cmd/flux/create_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) var createSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Create or update a Bucket source", Long: `The create source bucket command generates a Bucket resource and waits for it to be downloaded. For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`, Example: ` # Create a source for a Bucket using static authentication flux create source bucket podinfo \ --bucket-name=podinfo \ --endpoint=minio.minio.svc.cluster.local:9000 \ --insecure=true \ --access-key=myaccesskey \ --secret-key=mysecretkey \ --interval=10m # Create a source for an Amazon S3 Bucket using IAM authentication flux create source bucket podinfo \ --bucket-name=podinfo \ --provider=aws \ --endpoint=s3.amazonaws.com \ --region=us-east-1 \ --interval=10m`, RunE: createSourceBucketCmdRun, } type sourceBucketFlags struct { name string provider flags.SourceBucketProvider endpoint string accessKey string secretKey string region string insecure bool secretRef string proxySecretRef string ignorePaths []string } var sourceBucketArgs = newSourceBucketFlags() func init() { createSourceBucketCmd.Flags().Var(&sourceBucketArgs.provider, "provider", sourceBucketArgs.provider.Description()) createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.name, "bucket-name", "", "the bucket name") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.endpoint, "endpoint", "", "the bucket endpoint address") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.accessKey, "access-key", "", "the bucket access key") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretKey, "secret-key", "", "the bucket secret key") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region") createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials") createSourceBucketCmd.Flags().StringSliceVar(&sourceBucketArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in bucket resource (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceBucketCmd) } func newSourceBucketFlags() sourceBucketFlags { return sourceBucketFlags{ provider: flags.SourceBucketProvider(sourcev1.BucketProviderGeneric), } } func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if sourceBucketArgs.name == "" { return fmt.Errorf("bucket-name is required") } if sourceBucketArgs.endpoint == "" { return fmt.Errorf("endpoint is required") } sourceLabels, err := parseLabels() if err != nil { return err } var ignorePaths *string if len(sourceBucketArgs.ignorePaths) > 0 { ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n") ignorePaths = &ignorePathsStr } bucket := &sourcev1.Bucket{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.BucketSpec{ BucketName: sourceBucketArgs.name, Provider: sourceBucketArgs.provider.String(), Insecure: sourceBucketArgs.insecure, Endpoint: sourceBucketArgs.endpoint, Region: sourceBucketArgs.region, Interval: metav1.Duration{ Duration: createArgs.interval, }, Ignore: ignorePaths, }, } if createSourceArgs.fetchTimeout > 0 { bucket.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } if sourceBucketArgs.secretRef != "" { bucket.Spec.SecretRef = &meta.LocalObjectReference{ Name: sourceBucketArgs.secretRef, } } if sourceBucketArgs.proxySecretRef != "" { bucket.Spec.ProxySecretRef = &meta.LocalObjectReference{ Name: sourceBucketArgs.proxySecretRef, } } if createArgs.export { return printExport(exportBucket(bucket)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Generatef("generating Bucket source") if sourceBucketArgs.secretRef == "" { secretName := fmt.Sprintf("bucket-%s", name) secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, StringData: map[string]string{}, } if sourceBucketArgs.accessKey != "" && sourceBucketArgs.secretKey != "" { secret.StringData["accesskey"] = sourceBucketArgs.accessKey secret.StringData["secretkey"] = sourceBucketArgs.secretKey } if len(secret.StringData) > 0 { logger.Actionf("applying secret with the bucket credentials") if err := upsertSecret(ctx, kubeClient, secret); err != nil { return err } bucket.Spec.SecretRef = &meta.LocalObjectReference{ Name: secretName, } logger.Successf("authentication configured") } } logger.Actionf("applying Bucket source") namespacedName, err := upsertBucket(ctx, kubeClient, bucket) if err != nil { return err } logger.Waitingf("waiting for Bucket source reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, bucket)); err != nil { return err } logger.Successf("Bucket source reconciliation completed") if bucket.Status.Artifact == nil { return fmt.Errorf("Bucket source reconciliation but no artifact was found") } logger.Successf("fetched revision: %s", bucket.Status.Artifact.Revision) return nil } func upsertBucket(ctx context.Context, kubeClient client.Client, bucket *sourcev1.Bucket) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: bucket.GetNamespace(), Name: bucket.GetName(), } var existing sourcev1.Bucket err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, bucket); err != nil { return namespacedName, err } else { logger.Successf("Bucket source created") return namespacedName, nil } } return namespacedName, err } existing.Labels = bucket.Labels existing.Spec = bucket.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } bucket = &existing logger.Successf("Bucket source updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_source_chart.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) var createSourceChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Create or update a HelmChart source", Long: `The create source chart command generates a HelmChart resource and waits for the chart to be available.`, Example: ` # Create a source for a chart residing in a HelmRepository flux create source chart podinfo \ --source=HelmRepository/podinfo \ --chart=podinfo \ --chart-version=6.x # Create a source for a chart residing in a Git repository flux create source chart podinfo \ --source=GitRepository/podinfo \ --chart=./charts/podinfo # Create a source for a chart residing in a S3 Bucket flux create source chart podinfo \ --source=Bucket/podinfo \ --chart=./charts/podinfo # Create a source for a chart from OCI and verify its signature flux create source chart podinfo \ --source HelmRepository/podinfo \ --chart podinfo \ --chart-version=6.6.2 \ --verify-provider=cosign \ --verify-issuer=https://token.actions.githubusercontent.com \ --verify-subject=https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2`, RunE: createSourceChartCmdRun, } type sourceChartFlags struct { chart string chartVersion string source flags.LocalHelmChartSource reconcileStrategy string verifyProvider flags.SourceOCIVerifyProvider verifySecretRef string verifyOIDCIssuer string verifySubject string } var sourceChartArgs sourceChartFlags func init() { createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chart, "chart", "", "Helm chart name or path") createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)") createSourceChartCmd.Flags().Var(&sourceChartArgs.source, "source", sourceChartArgs.source.Description()) createSourceChartCmd.Flags().StringVar(&sourceChartArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart (accepted values: Revision and ChartRevision)") createSourceChartCmd.Flags().Var(&sourceChartArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description()) createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification") createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification") createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") createSourceCmd.AddCommand(createSourceChartCmd) } func createSourceChartCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if sourceChartArgs.source.Kind == "" || sourceChartArgs.source.Name == "" { return fmt.Errorf("chart source is required") } if sourceChartArgs.chart == "" { return fmt.Errorf("chart name or path is required") } logger.Generatef("generating HelmChart source") sourceLabels, err := parseLabels() if err != nil { return err } helmChart := &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.HelmChartSpec{ Chart: sourceChartArgs.chart, Version: sourceChartArgs.chartVersion, Interval: metav1.Duration{ Duration: createArgs.interval, }, ReconcileStrategy: sourceChartArgs.reconcileStrategy, SourceRef: sourcev1.LocalHelmChartSourceReference{ Kind: sourceChartArgs.source.Kind, Name: sourceChartArgs.source.Name, }, }, } if provider := sourceChartArgs.verifyProvider.String(); provider != "" { helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{ Provider: provider, } if secretName := sourceChartArgs.verifySecretRef; secretName != "" { helmChart.Spec.Verify.SecretRef = &meta.LocalObjectReference{ Name: secretName, } } verifyIssuer := sourceChartArgs.verifyOIDCIssuer verifySubject := sourceChartArgs.verifySubject if verifyIssuer != "" || verifySubject != "" { helmChart.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{ Issuer: verifyIssuer, Subject: verifySubject, }} } } else if sourceChartArgs.verifySecretRef != "" { return fmt.Errorf("a verification provider must be specified when a secret is specified") } else if sourceChartArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" { return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified") } if createArgs.export { return printExport(exportHelmChart(helmChart)) } logger.Actionf("applying HelmChart source") ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName, err := upsertHelmChart(ctx, kubeClient, helmChart) if err != nil { return err } logger.Waitingf("waiting for HelmChart source reconciliation") readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmChart) if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { return err } logger.Successf("HelmChart source reconciliation completed") if helmChart.Status.Artifact == nil { return fmt.Errorf("HelmChart source reconciliation completed but no artifact was found") } logger.Successf("fetched revision: %s", helmChart.Status.Artifact.Revision) return nil } func upsertHelmChart(ctx context.Context, kubeClient client.Client, helmChart *sourcev1.HelmChart) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: helmChart.GetNamespace(), Name: helmChart.GetName(), } var existing sourcev1.HelmChart err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, helmChart); err != nil { return namespacedName, err } else { logger.Successf("source created") return namespacedName, nil } } return namespacedName, err } existing.Labels = helmChart.Labels existing.Spec = helmChart.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } helmChart = &existing logger.Successf("source updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_source_chart_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "testing" func TestCreateSourceChart(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setupSourceChart(t, tmpl) tests := []struct { name string args string assert assertFunc }{ { name: "missing name", args: "create source chart --export", assert: assertError("name is required"), }, { name: "missing source reference", args: "create source chart podinfo --export ", assert: assertError("chart source is required"), }, { name: "missing chart name", args: "create source chart podinfo --source helmrepository/podinfo --export", assert: assertError("chart name or path is required"), }, { name: "unknown source kind", args: "create source chart podinfo --source foobar/podinfo --export", assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`), }, { name: "basic chart", args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --export", assert: assertGoldenTemplateFile("testdata/create_source_chart/basic.yaml", tmpl), }, { name: "chart with basic signature verification", args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --export", assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_basic.yaml", tmpl), }, { name: "unknown signature verification provider", args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider foobar --export", assert: assertError(`invalid argument "foobar" for "--verify-provider" flag: source OCI verify provider 'foobar' is not supported, must be one of: cosign`), }, { name: "chart with complete signature verification", args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --verify-issuer foo --verify-subject bar --export", assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_complete.yaml", tmpl), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: tt.assert, } cmd.runTestCmd(t) }) } } func setupSourceChart(t *testing.T, tmpl map[string]string) { t.Helper() testEnv.CreateObjectFile("./testdata/create_source_chart/setup-source.yaml", tmpl, t) } ================================================ FILE: cmd/flux/create_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "crypto/elliptic" "fmt" "net/url" "os" "strings" "github.com/manifoldco/promptui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) type sourceGitFlags struct { url string branch string tag string semver string refName string commit string username string password string keyAlgorithm flags.PublicKeyAlgorithm keyRSABits flags.RSAKeyBits keyECDSACurve flags.ECDSACurve secretRef string proxySecretRef string provider flags.SourceGitProvider caFile string privateKeyFile string recurseSubmodules bool silent bool ignorePaths []string sparseCheckoutPaths []string } var createSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Create or update a GitRepository source", Long: `The create source git command generates a GitRepository resource and waits for it to sync. For Git over SSH, host and SSH keys are automatically generated and stored in a Kubernetes secret. For private Git repositories, the basic authentication credentials are stored in a Kubernetes secret.`, Example: ` # Create a source from a public Git repository master branch flux create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ --branch=master # Create a source for a Git repository pinned to specific git tag flux create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ --tag="3.2.3" # Create a source from a public Git repository tag that matches a semver range flux create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ --tag-semver=">=3.2.0 <3.3.0" # Create a source for a Git repository using SSH authentication flux create source git podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \ --branch=master # Create a source for a Git repository using SSH authentication and an # ECDSA P-521 curve public key flux create source git podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \ --branch=master \ --ssh-key-algorithm=ecdsa \ --ssh-ecdsa-curve=p521 # Create a source for a Git repository using SSH authentication and a # passwordless private key from file # The public SSH host key will still be gathered from the host flux create source git podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \ --branch=master \ --private-key-file=./private.key # Create a source for a Git repository using SSH authentication and a # private key with a password from file # The public SSH host key will still be gathered from the host flux create source git podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \ --branch=master \ --private-key-file=./private.key \ --password= # Create a source for a Git repository using basic authentication flux create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ --branch=master \ --username=username \ --password=password # Create a source for a Git repository using azure provider flux create source git podinfo \ --url=https://dev.azure.com/foo/bar/_git/podinfo \ --branch=master \ --provider=azure`, RunE: createSourceGitCmdRun, } var sourceGitArgs = newSourceGitFlags() func init() { createSourceGitCmd.Flags().StringVar(&sourceGitArgs.url, "url", "", "git address, e.g. ssh://git@host/org/repository") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.branch, "branch", "", "git branch") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.tag, "tag", "", "git tag") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.semver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.refName, "ref-name", "", "git reference name") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.commit, "commit", "", "git commit") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.username, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.password, "password", "p", "", "basic authentication password") createSourceGitCmd.Flags().Var(&sourceGitArgs.keyAlgorithm, "ssh-key-algorithm", sourceGitArgs.keyAlgorithm.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyRSABits, "ssh-rsa-bits", sourceGitArgs.keyRSABits.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyECDSACurve, "ssh-ecdsa-curve", sourceGitArgs.keyECDSACurve.Description()) createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials or github app authentication") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials") createSourceGitCmd.Flags().Var(&sourceGitArgs.provider, "provider", sourceGitArgs.provider.Description()) createSourceGitCmd.Flags().StringVar(&sourceGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") createSourceGitCmd.Flags().BoolVar(&sourceGitArgs.recurseSubmodules, "recurse-submodules", false, "when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces") createSourceGitCmd.Flags().BoolVarP(&sourceGitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation") createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in git resource (can specify multiple paths with commas: path1,path2)") createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.sparseCheckoutPaths, "sparse-checkout-paths", nil, "set paths to sparse checkout in git resource (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceGitCmd) } func newSourceGitFlags() sourceGitFlags { return sourceGitFlags{ keyAlgorithm: flags.PublicKeyAlgorithm(sourcesecret.ECDSAPrivateKeyAlgorithm), keyRSABits: 2048, keyECDSACurve: flags.ECDSACurve{Curve: elliptic.P384()}, } } func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if sourceGitArgs.url == "" { return fmt.Errorf("url is required") } u, err := url.Parse(sourceGitArgs.url) if err != nil { return fmt.Errorf("git URL parse failed: %w", err) } if u.Scheme != "ssh" && u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) } if sourceGitArgs.branch == "" && sourceGitArgs.tag == "" && sourceGitArgs.semver == "" && sourceGitArgs.commit == "" && sourceGitArgs.refName == "" { return fmt.Errorf("a Git ref is required, use one of the following: --branch, --tag, --commit, --ref-name or --tag-semver") } if sourceGitArgs.caFile != "" && u.Scheme == "ssh" { return fmt.Errorf("specifying a CA file is not supported for Git over SSH") } sourceLabels, err := parseLabels() if err != nil { return err } var ignorePaths *string if len(sourceGitArgs.ignorePaths) > 0 { ignorePathsStr := strings.Join(sourceGitArgs.ignorePaths, "\n") ignorePaths = &ignorePathsStr } gitRepository := sourcev1.GitRepository{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.GitRepositorySpec{ URL: sourceGitArgs.url, Interval: metav1.Duration{ Duration: createArgs.interval, }, RecurseSubmodules: sourceGitArgs.recurseSubmodules, Reference: &sourcev1.GitRepositoryRef{}, Ignore: ignorePaths, SparseCheckout: sourceGitArgs.sparseCheckoutPaths, }, } if createSourceArgs.fetchTimeout > 0 { gitRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } if sourceGitArgs.commit != "" { gitRepository.Spec.Reference.Commit = sourceGitArgs.commit gitRepository.Spec.Reference.Branch = sourceGitArgs.branch } else if sourceGitArgs.refName != "" { gitRepository.Spec.Reference.Name = sourceGitArgs.refName } else if sourceGitArgs.semver != "" { gitRepository.Spec.Reference.SemVer = sourceGitArgs.semver } else if sourceGitArgs.tag != "" { gitRepository.Spec.Reference.Tag = sourceGitArgs.tag } else { gitRepository.Spec.Reference.Branch = sourceGitArgs.branch } if sourceGitArgs.secretRef != "" { gitRepository.Spec.SecretRef = &meta.LocalObjectReference{ Name: sourceGitArgs.secretRef, } } if sourceGitArgs.proxySecretRef != "" { gitRepository.Spec.ProxySecretRef = &meta.LocalObjectReference{ Name: sourceGitArgs.proxySecretRef, } } if provider := sourceGitArgs.provider.String(); provider != "" { gitRepository.Spec.Provider = provider } if createArgs.export { return printExport(exportGit(&gitRepository)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Generatef("generating GitRepository source") if sourceGitArgs.secretRef == "" { secretOpts := sourcesecret.Options{ Name: name, Namespace: *kubeconfigArgs.Namespace, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } switch u.Scheme { case "ssh": keypair, err := sourcesecret.LoadKeyPairFromPath(sourceGitArgs.privateKeyFile, sourceGitArgs.password) if err != nil { return err } secretOpts.Keypair = keypair secretOpts.SSHHostname = u.Host secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits) secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve secretOpts.Password = sourceGitArgs.password case "https": if sourceGitArgs.caFile != "" { caBundle, err := os.ReadFile(sourceGitArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } secretOpts.CACrt = caBundle } secretOpts.Username = sourceGitArgs.username secretOpts.Password = sourceGitArgs.password case "http": logger.Warningf("insecure configuration: credentials configured for an HTTP URL") secretOpts.Username = sourceGitArgs.username secretOpts.Password = sourceGitArgs.password } secret, err := sourcesecret.GenerateGit(secretOpts) if err != nil { return err } var s corev1.Secret if err = yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if len(s.StringData) > 0 { if hk, ok := s.StringData[sourcesecret.KnownHostsSecretKey]; ok { logger.Successf("collected public key from SSH server:\n%s", hk) } if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok { logger.Generatef("deploy key: %s", ppk) if !sourceGitArgs.silent { prompt := promptui.Prompt{ Label: "Have you added the deploy key to your repository", IsConfirm: true, } if _, err := prompt.Run(); err != nil { return fmt.Errorf("aborting") } } } logger.Actionf("applying secret with repository credentials") if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } gitRepository.Spec.SecretRef = &meta.LocalObjectReference{ Name: s.Name, } logger.Successf("authentication configured") } } logger.Actionf("applying GitRepository source") namespacedName, err := upsertGitRepository(ctx, kubeClient, &gitRepository) if err != nil { return err } logger.Waitingf("waiting for GitRepository source reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, &gitRepository)); err != nil { return err } logger.Successf("GitRepository source reconciliation completed") if gitRepository.Status.Artifact == nil { return fmt.Errorf("GitRepository source reconciliation completed but no artifact was found") } logger.Successf("fetched revision: %s", gitRepository.Status.Artifact.Revision) return nil } func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepository *sourcev1.GitRepository) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: gitRepository.GetNamespace(), Name: gitRepository.GetName(), } var existing sourcev1.GitRepository err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, gitRepository); err != nil { return namespacedName, err } else { logger.Successf("GitRepository source created") return namespacedName, nil } } return namespacedName, err } existing.Labels = gitRepository.Labels existing.Spec = gitRepository.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } gitRepository = &existing logger.Successf("GitRepository source updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_source_git_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "testing" "time" "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var pollInterval = 50 * time.Millisecond var testTimeout = 10 * time.Second // Update the GitRepository once created to exercise test specific behavior type reconcileFunc func(repo *sourcev1.GitRepository) // reconciler waits for an object to be created, then invokes a test supplied // function to mutate that object, simulating a controller. // Test should invoke run() to run the background reconciler task which // polls to wait for the object to exist before applying the update function. // Any errors from the reconciler are asserted on test completion. type reconciler struct { client client.Client name types.NamespacedName reconcile reconcileFunc } // Start the background task that waits for the object to exist then applies // the update function. func (r *reconciler) run(t *testing.T) { result := make(chan error) go func() { defer close(result) err := wait.PollImmediate( pollInterval, testTimeout, r.conditionFunc) result <- err }() t.Cleanup(func() { if err := <-result; err != nil { t.Errorf("Failure from test reconciler: '%v':", err.Error()) } }) } // A ConditionFunction that waits for the named GitRepository to be created, // then sets the ready condition to true. func (r *reconciler) conditionFunc() (bool, error) { var repo sourcev1.GitRepository if err := r.client.Get(context.Background(), r.name, &repo); err != nil { if errors.IsNotFound(err) { return false, nil // Keep polling until object is created } return true, err } r.reconcile(&repo) err := r.client.Status().Update(context.Background(), &repo) return true, err } func TestCreateSourceGitExport(t *testing.T) { var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --sparse-checkout-paths .cosign,non-existent-dir/ --ignore-paths .cosign,non-existent-dir/ -n default --interval 1m --export --timeout=" + testTimeout.String() cases := []struct { name string args string assert assertFunc }{ { "ExportSucceeded", command, assertGoldenFile("testdata/create_source_git/export.golden"), }, { name: "no args", args: "create source git --url=https://github.com/stefanprodan/podinfo", assert: assertError("name is required"), }, { name: "source with commit", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --commit=c88a2f41 --interval=1m0s --export", assert: assertGoldenFile("./testdata/create_source_git/source-git-commit.yaml"), }, { name: "source with ref name", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --ref-name=refs/heads/main --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-refname.yaml"), }, { name: "source with branch name and commit", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=main --commit=c88a2f41 --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-branch-commit.yaml"), }, { name: "source with semver", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag-semver=v1.01 --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-semver.yaml"), }, { name: "source with git tag", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag=test --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-tag.yaml"), }, { name: "source with git branch", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=test --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-branch.yaml"), }, { name: "source with generic provider", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --provider generic --branch=test --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-provider-generic.yaml"), }, { name: "source with azure provider", args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider azure --branch=test --interval=1m0s --export", assert: assertGoldenFile("testdata/create_source_git/source-git-provider-azure.yaml"), }, { name: "source with github provider", args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --provider github --branch=test --interval=1m0s --secret-ref appinfo --export", assert: assertGoldenFile("testdata/create_source_git/source-git-provider-github.yaml"), }, { name: "source with invalid provider", args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export", assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|azure|github"), }, { name: "source with empty provider", args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider \"\" --branch=test --interval=1m0s --export", assert: assertError("invalid argument \"\" for \"--provider\" flag: no source Git provider given, please specify the Git provider name"), }, { name: "source with no provider", args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --branch=test --interval=1m0s --export --provider", assert: assertError("flag needs an argument: --provider"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cmd := cmdTestCase{ args: tc.args, assert: tc.assert, } cmd.runTestCmd(t) }) } } func TestCreateSourceGit(t *testing.T) { // Default command used for multiple tests var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --timeout=" + testTimeout.String() cases := []struct { name string args string assert assertFunc reconcile reconcileFunc }{ { "NoArgs", "create source git", assertError("name is required"), nil, }, { "Succeeded", command, assertGoldenFile("testdata/create_source_git/success.golden"), func(repo *sourcev1.GitRepository) { newCondition := metav1.Condition{ Type: meta.ReadyCondition, Status: metav1.ConditionTrue, Reason: sourcev1.GitOperationSucceedReason, Message: "succeeded message", ObservedGeneration: repo.GetGeneration(), } apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) repo.Status.Artifact = &meta.Artifact{ Path: "some-path", Revision: "v1", LastUpdateTime: metav1.Time{ Time: time.Now(), }, Digest: "sha256:1234567890abcdef", } repo.Status.ObservedGeneration = repo.GetGeneration() }, }, { "Failed", command, assertError("failed message"), func(repo *sourcev1.GitRepository) { stalledCondition := metav1.Condition{ Type: meta.StalledCondition, Status: metav1.ConditionTrue, Reason: sourcev1.URLInvalidReason, Message: "failed message", ObservedGeneration: repo.GetGeneration(), } apimeta.SetStatusCondition(&repo.Status.Conditions, stalledCondition) newCondition := metav1.Condition{ Type: meta.ReadyCondition, Status: metav1.ConditionFalse, Reason: sourcev1.URLInvalidReason, Message: "failed message", ObservedGeneration: repo.GetGeneration(), } apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) repo.Status.ObservedGeneration = repo.GetGeneration() }, }, { "NoArtifact", command, assertError("GitRepository source reconciliation completed but no artifact was found"), func(repo *sourcev1.GitRepository) { // Updated with no artifact newCondition := metav1.Condition{ Type: meta.ReadyCondition, Status: metav1.ConditionTrue, Reason: sourcev1.GitOperationSucceedReason, Message: "succeeded message", ObservedGeneration: repo.GetGeneration(), } apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) repo.Status.ObservedGeneration = repo.GetGeneration() }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ns := allocateNamespace("podinfo") setupTestNamespace(ns, t) if tc.reconcile != nil { r := reconciler{ client: testEnv.client, name: types.NamespacedName{Namespace: ns, Name: "podinfo"}, reconcile: tc.reconcile, } r.run(t) } cmd := cmdTestCase{ args: tc.args + " -n=" + ns, assert: tc.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "net/url" "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Create or update a HelmRepository source", Long: `The create source helm command generates a HelmRepository resource and waits for it to fetch the index. For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`, Example: ` # Create a source for an HTTPS public Helm repository flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --interval=10m # Create a source for an HTTPS Helm repository using basic authentication flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --username=username \ --password=password # Create a source for an HTTPS Helm repository using TLS authentication flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --cert-file=./cert.crt \ --key-file=./key.crt \ --ca-file=./ca.crt # Create a source for an OCI Helm repository flux create source helm podinfo \ --url=oci://ghcr.io/stefanprodan/charts/podinfo \ --username=username \ --password=password # Create a source for an OCI Helm repository using an existing secret with basic auth or dockerconfig credentials flux create source helm podinfo \ --url=oci://ghcr.io/stefanprodan/charts/podinfo \ --secret-ref=docker-config`, RunE: createSourceHelmCmdRun, } type sourceHelmFlags struct { url string username string password string certFile string keyFile string caFile string secretRef string ociProvider string passCredentials bool } var sourceHelmArgs sourceHelmFlags func init() { createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.url, "url", "", "Helm repository address") createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.username, "username", "u", "", "basic authentication username") createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.password, "password", "p", "", "basic authentication password") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.certFile, "cert-file", "", "TLS authentication cert file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.keyFile, "key-file", "", "TLS authentication key file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.caFile, "ca-file", "", "TLS authentication CA file path") createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.secretRef, "secret-ref", "", "", "the name of an existing secret containing TLS, basic auth or docker-config credentials") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.ociProvider, "oci-provider", "", "OCI provider for authentication") createSourceHelmCmd.Flags().BoolVarP(&sourceHelmArgs.passCredentials, "pass-credentials", "", false, "pass credentials to all domains") createSourceCmd.AddCommand(createSourceHelmCmd) } func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if sourceHelmArgs.url == "" { return fmt.Errorf("url is required") } sourceLabels, err := parseLabels() if err != nil { return err } if _, err := url.Parse(sourceHelmArgs.url); err != nil { return fmt.Errorf("url parse failed: %w", err) } helmRepository := &sourcev1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.HelmRepositorySpec{ URL: sourceHelmArgs.url, Interval: metav1.Duration{ Duration: createArgs.interval, }, }, } url, err := url.Parse(sourceHelmArgs.url) if err != nil { return fmt.Errorf("failed to parse URL: %w", err) } if url.Scheme == sourcev1.HelmRepositoryTypeOCI { helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI helmRepository.Spec.Provider = sourceHelmArgs.ociProvider } if createSourceArgs.fetchTimeout > 0 { helmRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } if sourceHelmArgs.secretRef != "" { helmRepository.Spec.SecretRef = &meta.LocalObjectReference{ Name: sourceHelmArgs.secretRef, } helmRepository.Spec.PassCredentials = sourceHelmArgs.passCredentials } if createArgs.export { return printExport(exportHelmRepository(helmRepository)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } caBundle := []byte{} if sourceHelmArgs.caFile != "" { var err error caBundle, err = os.ReadFile(sourceHelmArgs.caFile) if err != nil { return fmt.Errorf("unable to read TLS CA file: %w", err) } } var certFile, keyFile []byte if sourceHelmArgs.certFile != "" && sourceHelmArgs.keyFile != "" { if certFile, err = os.ReadFile(sourceHelmArgs.certFile); err != nil { return fmt.Errorf("failed to read cert file: %w", err) } if keyFile, err = os.ReadFile(sourceHelmArgs.keyFile); err != nil { return fmt.Errorf("failed to read key file: %w", err) } } logger.Generatef("generating HelmRepository source") if sourceHelmArgs.secretRef == "" { secretName := fmt.Sprintf("helm-%s", name) secretOpts := sourcesecret.Options{ Name: secretName, Namespace: *kubeconfigArgs.Namespace, Username: sourceHelmArgs.username, Password: sourceHelmArgs.password, CACrt: caBundle, TLSCrt: certFile, TLSKey: keyFile, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } secret, err := sourcesecret.GenerateHelm(secretOpts) if err != nil { return err } var s corev1.Secret if err = yaml.Unmarshal([]byte(secret.Content), &s); err != nil { return err } if len(s.StringData) > 0 { logger.Actionf("applying secret with repository credentials") if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } helmRepository.Spec.SecretRef = &meta.LocalObjectReference{ Name: secretName, } helmRepository.Spec.PassCredentials = sourceHelmArgs.passCredentials logger.Successf("authentication configured") } } logger.Actionf("applying HelmRepository source") namespacedName, err := upsertHelmRepository(ctx, kubeClient, helmRepository) if err != nil { return err } logger.Waitingf("waiting for HelmRepository source reconciliation") readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository) if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI { // HelmRepository type OCI is a static object. readyConditionFunc = isStaticObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository) } if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { return err } logger.Successf("HelmRepository source reconciliation completed") if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI { // OCI repos don't expose any artifact so we just return early here return nil } if helmRepository.Status.Artifact == nil { return fmt.Errorf("HelmRepository source reconciliation completed but no artifact was found") } logger.Successf("fetched revision: %s", helmRepository.Status.Artifact.Revision) return nil } func upsertHelmRepository(ctx context.Context, kubeClient client.Client, helmRepository *sourcev1.HelmRepository) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: helmRepository.GetNamespace(), Name: helmRepository.GetName(), } var existing sourcev1.HelmRepository err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, helmRepository); err != nil { return namespacedName, err } else { logger.Successf("source created") return namespacedName, nil } } return namespacedName, err } existing.Labels = helmRepository.Labels existing.Spec = helmRepository.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } helmRepository = &existing logger.Successf("source updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_source_helm_test.go ================================================ //go:build unit // +build unit /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateSourceHelm(t *testing.T) { tests := []struct { name string args string resultFile string assertFunc string }{ { name: "no args", args: "create source helm", resultFile: "name is required", assertFunc: "assertError", }, { name: "OCI repo", args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export", resultFile: "./testdata/create_source_helm/oci.golden", assertFunc: "assertGoldenTemplateFile", }, { name: "OCI repo with Secret ref", args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --secret-ref=creds --export", resultFile: "./testdata/create_source_helm/oci-with-secret.golden", assertFunc: "assertGoldenTemplateFile", }, { name: "HTTPS repo", args: "create source helm podinfo --url=https://stefanprodan.github.io/charts/podinfo --interval 5m --export", resultFile: "./testdata/create_source_helm/https.golden", assertFunc: "assertGoldenTemplateFile", }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } setup(t, tmpl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var assert assertFunc switch tt.assertFunc { case "assertGoldenTemplateFile": assert = assertGoldenTemplateFile(tt.resultFile, tmpl) case "assertError": assert = assertError(tt.resultFile) } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "strings" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) var createSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Create or update an OCIRepository", Long: withPreviewNote(`The create source oci command generates an OCIRepository resource and waits for it to be ready.`), Example: ` # Create an OCIRepository for a public container image flux create source oci podinfo \ --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag=6.6.2 \ --interval=10m # Create an OCIRepository with OIDC signature verification flux create source oci podinfo \ --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag=6.6.2 \ --interval=10m \ --verify-provider=cosign \ --verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \ --verify-issuer="^https://token.actions.githubusercontent.com$" `, RunE: createSourceOCIRepositoryCmdRun, } type sourceOCIRepositoryFlags struct { url string tag string semver string digest string secretRef string proxySecretRef string serviceAccount string certSecretRef string verifyProvider flags.SourceOCIVerifyProvider verifySecretRef string verifyOIDCIssuer string verifySubject string ignorePaths []string provider flags.SourceOCIProvider insecure bool } var sourceOCIRepositoryArgs = newSourceOCIFlags() func newSourceOCIFlags() sourceOCIRepositoryFlags { return sourceOCIRepositoryFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } func init() { createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates") createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description()) createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) } func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if sourceOCIRepositoryArgs.url == "" { return fmt.Errorf("url is required") } if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" { return fmt.Errorf("--tag, --tag-semver or --digest is required") } sourceLabels, err := parseLabels() if err != nil { return err } var ignorePaths *string if len(sourceOCIRepositoryArgs.ignorePaths) > 0 { ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n") ignorePaths = &ignorePathsStr } repository := &sourcev1.OCIRepository{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.OCIRepositorySpec{ Provider: sourceOCIRepositoryArgs.provider.String(), URL: sourceOCIRepositoryArgs.url, Insecure: sourceOCIRepositoryArgs.insecure, Interval: metav1.Duration{ Duration: createArgs.interval, }, Reference: &sourcev1.OCIRepositoryRef{}, Ignore: ignorePaths, }, } if digest := sourceOCIRepositoryArgs.digest; digest != "" { repository.Spec.Reference.Digest = digest } if semver := sourceOCIRepositoryArgs.semver; semver != "" { repository.Spec.Reference.SemVer = semver } if tag := sourceOCIRepositoryArgs.tag; tag != "" { repository.Spec.Reference.Tag = tag } if createSourceArgs.fetchTimeout > 0 { repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" { repository.Spec.ServiceAccountName = saName } if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" { repository.Spec.SecretRef = &meta.LocalObjectReference{ Name: secretName, } } if secretName := sourceOCIRepositoryArgs.proxySecretRef; secretName != "" { repository.Spec.ProxySecretRef = &meta.LocalObjectReference{ Name: secretName, } } if secretName := sourceOCIRepositoryArgs.certSecretRef; secretName != "" { repository.Spec.CertSecretRef = &meta.LocalObjectReference{ Name: secretName, } } if provider := sourceOCIRepositoryArgs.verifyProvider.String(); provider != "" { repository.Spec.Verify = &sourcev1.OCIRepositoryVerification{ Provider: provider, } if secretName := sourceOCIRepositoryArgs.verifySecretRef; secretName != "" { repository.Spec.Verify.SecretRef = &meta.LocalObjectReference{ Name: secretName, } } verifyIssuer := sourceOCIRepositoryArgs.verifyOIDCIssuer verifySubject := sourceOCIRepositoryArgs.verifySubject if verifyIssuer != "" || verifySubject != "" { repository.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{ Issuer: verifyIssuer, Subject: verifySubject, }} } } else if sourceOCIRepositoryArgs.verifySecretRef != "" { return fmt.Errorf("a verification provider must be specified when a secret is specified") } else if sourceOCIRepositoryArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" { return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified") } if createArgs.export { return printExport(exportOCIRepository(repository)) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } logger.Actionf("applying OCIRepository") namespacedName, err := upsertOCIRepository(ctx, kubeClient, repository) if err != nil { return err } logger.Waitingf("waiting for OCIRepository reconciliation") if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil { return err } logger.Successf("OCIRepository reconciliation completed") if repository.Status.Artifact == nil { return fmt.Errorf("no artifact was found") } logger.Successf("fetched revision: %s", repository.Status.Artifact.Revision) return nil } func upsertOCIRepository(ctx context.Context, kubeClient client.Client, ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: ociRepository.GetNamespace(), Name: ociRepository.GetName(), } var existing sourcev1.OCIRepository err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, ociRepository); err != nil { return namespacedName, err } else { logger.Successf("OCIRepository created") return namespacedName, nil } } return namespacedName, err } existing.Labels = ociRepository.Labels existing.Spec = ociRepository.Spec if err := kubeClient.Update(ctx, &existing); err != nil { return namespacedName, err } ociRepository = &existing logger.Successf("OCIRepository updated") return namespacedName, nil } ================================================ FILE: cmd/flux/create_source_oci_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateSourceOCI(t *testing.T) { tests := []struct { name string args string assertFunc assertFunc }{ { name: "NoArgs", args: "create source oci", assertFunc: assertError("name is required"), }, { name: "NoURL", args: "create source oci podinfo", assertFunc: assertError("url is required"), }, { name: "verify secret specified but provider missing", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub", assertFunc: assertError("a verification provider must be specified when a secret is specified"), }, { name: "verify issuer specified but provider missing", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github.com", assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"), }, { name: "verify identity specified but provider missing", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=developer", assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"), }, { name: "verify issuer specified but subject missing", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github --verify-provider=cosign --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"), }, { name: "all verify fields set", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github verify-subject=stefanprodan --verify-provider=cosign --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"), }, { name: "verify subject specified but issuer missing", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=stefanprodan --verify-provider=cosign --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_subject.golden"), }, { name: "export manifest", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export", assertFunc: assertGoldenFile("./testdata/oci/export.golden"), }, { name: "export manifest with secret", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --secret-ref=creds --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"), }, { name: "export manifest with verify secret", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assertFunc, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_tenant.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" ) var createTenantCmd = &cobra.Command{ Use: "tenant", Short: "Create or update a tenant", Long: withPreviewNote(`The create tenant command generates namespaces, service accounts and role bindings to limit the reconcilers scope to the tenant namespaces.`), Example: ` # Create a tenant with access to a namespace flux create tenant dev-team \ --with-namespace=frontend \ --label=environment=dev # Generate tenant namespaces and role bindings in YAML format flux create tenant dev-team \ --with-namespace=frontend \ --with-namespace=backend \ --export > dev-team.yaml`, RunE: createTenantCmdRun, } const ( tenantLabel = "toolkit.fluxcd.io/tenant" ) type tenantFlags struct { namespaces []string clusterRole string account string skipNamespace bool } var tenantArgs tenantFlags func init() { createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant") createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding") createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant") createTenantCmd.Flags().BoolVar(&tenantArgs.skipNamespace, "skip-namespace", false, "skip namespace creation (namespace must exist already)") createCmd.AddCommand(createTenantCmd) } func createTenantCmdRun(cmd *cobra.Command, args []string) error { tenant := args[0] if err := validation.IsQualifiedName(tenant); len(err) > 0 { return fmt.Errorf("invalid tenant name '%s': %v", tenant, err) } if tenantArgs.clusterRole == "" { return fmt.Errorf("cluster-role is required") } if tenantArgs.namespaces == nil { return fmt.Errorf("with-namespace is required") } var namespaces []corev1.Namespace var accounts []corev1.ServiceAccount var roleBindings []rbacv1.RoleBinding for _, ns := range tenantArgs.namespaces { if err := validation.IsQualifiedName(ns); len(err) > 0 { return fmt.Errorf("invalid namespace '%s': %v", ns, err) } objLabels, err := parseLabels() if err != nil { return err } objLabels[tenantLabel] = tenant namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: ns, Labels: objLabels, }, } namespaces = append(namespaces, namespace) accountName := tenant if tenantArgs.account != "" { accountName = tenantArgs.account } if err := validation.IsQualifiedName(accountName); len(err) > 0 { return fmt.Errorf("invalid service-account name '%s': %v", accountName, err) } account := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: accountName, Namespace: ns, Labels: objLabels, }, } accounts = append(accounts, account) roleBinding := rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-reconciler", tenant), Namespace: ns, Labels: objLabels, }, Subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: fmt.Sprintf("gotk:%s:reconciler", ns), }, { Kind: "ServiceAccount", Name: accountName, Namespace: ns, }, }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: tenantArgs.clusterRole, }, } roleBindings = append(roleBindings, roleBinding) } if createArgs.export { for i := range tenantArgs.namespaces { if err := exportTenant(namespaces[i], accounts[i], roleBindings[i], tenantArgs.skipNamespace); err != nil { return err } } return nil } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } for i := range tenantArgs.namespaces { if !tenantArgs.skipNamespace { logger.Actionf("applying namespace %s", namespaces[i].Name) if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil { return err } } logger.Actionf("applying service account %s", accounts[i].Name) if err := upsertServiceAccount(ctx, kubeClient, accounts[i]); err != nil { return err } logger.Actionf("applying role binding %s", roleBindings[i].Name) if err := upsertRoleBinding(ctx, kubeClient, roleBindings[i]); err != nil { return err } } logger.Successf("tenant setup completed") return nil } func upsertNamespace(ctx context.Context, kubeClient client.Client, namespace corev1.Namespace) error { namespacedName := types.NamespacedName{ Namespace: namespace.GetNamespace(), Name: namespace.GetName(), } var existing corev1.Namespace err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, &namespace); err != nil { return err } else { return nil } } return err } if !equality.Semantic.DeepDerivative(namespace.Labels, existing.Labels) { existing.Labels = namespace.Labels if err := kubeClient.Update(ctx, &existing); err != nil { return err } } return nil } func upsertServiceAccount(ctx context.Context, kubeClient client.Client, account corev1.ServiceAccount) error { namespacedName := types.NamespacedName{ Namespace: account.GetNamespace(), Name: account.GetName(), } var existing corev1.ServiceAccount err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, &account); err != nil { return err } else { return nil } } return err } if !equality.Semantic.DeepDerivative(account.Labels, existing.Labels) { existing.Labels = account.Labels if err := kubeClient.Update(ctx, &existing); err != nil { return err } } return nil } func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBinding rbacv1.RoleBinding) error { namespacedName := types.NamespacedName{ Namespace: roleBinding.GetNamespace(), Name: roleBinding.GetName(), } var existing rbacv1.RoleBinding err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { if err := kubeClient.Create(ctx, &roleBinding); err != nil { return err } else { return nil } } return err } if !equality.Semantic.DeepDerivative(roleBinding.Subjects, existing.Subjects) || !equality.Semantic.DeepDerivative(roleBinding.RoleRef, existing.RoleRef) || !equality.Semantic.DeepDerivative(roleBinding.Labels, existing.Labels) { if err := kubeClient.Delete(ctx, &existing); err != nil { return err } if err := kubeClient.Create(ctx, &roleBinding); err != nil { return err } } return nil } func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding, skipNamespace bool) error { var data []byte var err error if !skipNamespace { namespace.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", Kind: "Namespace", } data, err = yaml.Marshal(namespace) if err != nil { return err } data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1) printlnStdout("---") printlnStdout(resourceToString(data)) } account.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", Kind: "ServiceAccount", } data, err = yaml.Marshal(account) if err != nil { return err } data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1) printlnStdout("---") printlnStdout(resourceToString(data)) roleBinding.TypeMeta = metav1.TypeMeta{ APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding", } data, err = yaml.Marshal(roleBinding) if err != nil { return err } printlnStdout("---") printlnStdout(resourceToString(data)) return nil } ================================================ FILE: cmd/flux/create_tenant_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestCreateTenant(t *testing.T) { tests := []struct { name string args string assert assertFunc }{ { name: "no args", args: "create tenant", assert: assertError("name is required"), }, { name: "no namespace", args: "create tenant dev-team --cluster-role=cluster-admin", assert: assertError("with-namespace is required"), }, { name: "basic tenant", args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --export", assert: assertGoldenFile("./testdata/create_tenant/tenant-basic.yaml"), }, { name: "tenant with custom serviceaccount", args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --with-service-account=flux-tenant --export", assert: assertGoldenFile("./testdata/create_tenant/tenant-with-service-account.yaml"), }, { name: "tenant with custom cluster role", args: "create tenant dev-team --with-namespace=apps --cluster-role=custom-role --export", assert: assertGoldenFile("./testdata/create_tenant/tenant-with-cluster-role.yaml"), }, { name: "tenant with skip namespace", args: "create tenant dev-team --with-namespace=apps --cluster-role=cluster-admin --skip-namespace --export", assert: assertGoldenFile("./testdata/create_tenant/tenant-with-skip-namespace.yaml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/create_test.go ================================================ package main import ( "testing" "k8s.io/apimachinery/pkg/util/rand" ) func Test_validateObjectName(t *testing.T) { tests := []struct { name string valid bool }{ { name: "flux-system", valid: true, }, { name: "-flux-system", valid: false, }, { name: "-flux-system-", valid: false, }, { name: "third.first", valid: false, }, { name: "THirdfirst", valid: false, }, { name: "THirdfirst", valid: false, }, { name: rand.String(63), valid: true, }, { name: rand.String(64), valid: false, }, } for _, tt := range tests { valid := validateObjectName(tt.name) if valid != tt.valid { t.Errorf("expected name %q to return %t for validateObjectName func but got %t", tt.name, tt.valid, valid) } } } ================================================ FILE: cmd/flux/debug.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var debugCmd = &cobra.Command{ Use: "debug", Short: "Debug a flux resource", Long: `The debug command can be used to troubleshoot failing resource reconciliations.`, } func init() { rootCmd.AddCommand(debugCmd) } ================================================ FILE: cmd/flux/debug_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/pkg/chartutil" "github.com/go-logr/logr" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" ) var debugHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Debug a HelmRelease resource", Long: withPreviewNote(`The debug helmrelease command can be used to troubleshoot failing Helm release reconciliations. WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the HelmRelease .spec.valuesFrom field.`), Example: ` # Print the status of a Helm release flux debug hr podinfo --show-status # Export the final values of a Helm release composed from referred ConfigMaps and Secrets flux debug hr podinfo --show-values > values.yaml # Print the reconciliation history of a Helm release flux debug hr podinfo --show-history`, RunE: debugHelmReleaseCmdRun, Args: cobra.ExactArgs(1), ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), } type debugHelmReleaseFlags struct { showStatus bool showValues bool showHistory bool } var debugHelmReleaseArgs debugHelmReleaseFlags func init() { debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showStatus, "show-status", false, "print the status of the Helm release") debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showValues, "show-values", false, "print the final values of the Helm release") debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showHistory, "show-history", false, "print the reconciliation history of the Helm release") debugCmd.AddCommand(debugHelmReleaseCmd) } func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { name := args[0] flagsSet := 0 if debugHelmReleaseArgs.showStatus { flagsSet++ } if debugHelmReleaseArgs.showValues { flagsSet++ } if debugHelmReleaseArgs.showHistory { flagsSet++ } if flagsSet != 1 { return fmt.Errorf("exactly one of --show-status, --show-values, or --show-history must be set") } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } hr := &helmv2.HelmRelease{} hrName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name} if err := kubeClient.Get(ctx, hrName, hr); err != nil { return err } if debugHelmReleaseArgs.showStatus { status, err := yaml.Marshal(hr.Status) if err != nil { return err } rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/helm/helmreleases/#helmrelease-status") rootCmd.Print(string(status)) return nil } if debugHelmReleaseArgs.showValues { finalValues, err := chartutil.ChartValuesFromReferences(ctx, logr.Discard(), kubeClient, hr.GetNamespace(), hr.GetValues(), hr.Spec.ValuesFrom...) if err != nil { return err } values, err := yaml.Marshal(finalValues) if err != nil { return err } rootCmd.Print(string(values)) } if debugHelmReleaseArgs.showHistory { if len(hr.Status.History) == 0 { hr.Status.History = helmv2.Snapshots{} } history, err := yaml.Marshal(hr.Status.History) if err != nil { return err } rootCmd.Println("# History documentation: https://fluxcd.io/flux/components/helm/helmreleases/#history") rootCmd.Print(string(history)) return nil } return nil } ================================================ FILE: cmd/flux/debug_helmrelease_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestDebugHelmRelease(t *testing.T) { namespace := allocateNamespace("debug") objectFile := "testdata/debug_helmrelease/objects.yaml" tmpl := map[string]string{ "fluxns": namespace, } testEnv.CreateObjectFile(objectFile, tmpl, t) cases := []struct { name string arg string goldenFile string tmpl map[string]string }{ { "debug status", "debug helmrelease test-values-inline --show-status --show-values=false", "testdata/debug_helmrelease/status.golden.yaml", tmpl, }, { "debug values", "debug helmrelease test-values-inline --show-values --show-status=false", "testdata/debug_helmrelease/values-inline.golden.yaml", tmpl, }, { "debug values from", "debug helmrelease test-values-from --show-values --show-status=false", "testdata/debug_helmrelease/values-from.golden.yaml", tmpl, }, { "debug history", "debug helmrelease test-with-history --show-history --show-status=false", "testdata/debug_helmrelease/history.golden.yaml", tmpl, }, { "debug history empty", "debug helmrelease test-values-inline --show-history --show-status=false", "testdata/debug_helmrelease/history-empty.golden.yaml", tmpl, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.arg + " -n=" + namespace, assert: assertGoldenTemplateFile(tt.goldenFile, tmpl), } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/debug_kustomization.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "errors" "fmt" "sort" "strings" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/kustomize" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" ) var debugKustomizationCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Debug a Flux Kustomization resource", Long: withPreviewNote(`The debug kustomization command can be used to troubleshoot failing Flux Kustomization reconciliations. WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the Kustomization .spec.postBuild.substituteFrom field.`), Example: ` # Print the status of a Flux Kustomization flux debug ks podinfo --show-status # Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets flux debug ks podinfo --show-vars > vars.env # Print the reconciliation history of a Flux Kustomization flux debug ks podinfo --show-history`, RunE: debugKustomizationCmdRun, Args: cobra.ExactArgs(1), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), } type debugKustomizationFlags struct { showStatus bool showVars bool showHistory bool } var debugKustomizationArgs debugKustomizationFlags func init() { debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showStatus, "show-status", false, "print the status of the Flux Kustomization") debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showVars, "show-vars", false, "print the final vars of the Flux Kustomization in dot env format") debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showHistory, "show-history", false, "print the reconciliation history of the Flux Kustomization") debugCmd.AddCommand(debugKustomizationCmd) } func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error { name := args[0] flagsSet := 0 if debugKustomizationArgs.showStatus { flagsSet++ } if debugKustomizationArgs.showVars { flagsSet++ } if debugKustomizationArgs.showHistory { flagsSet++ } if flagsSet != 1 { return fmt.Errorf("exactly one of --show-status, --show-vars, or --show-history must be set") } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } ks := &kustomizev1.Kustomization{} ksName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name} if err := kubeClient.Get(ctx, ksName, ks); err != nil { return err } if debugKustomizationArgs.showStatus { status, err := yaml.Marshal(ks.Status) if err != nil { return err } rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status") rootCmd.Print(string(status)) return nil } if debugKustomizationArgs.showVars { if ks.Spec.PostBuild == nil { return errors.New("no post build substitutions found") } ksObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ks) if err != nil { return err } finalVars, err := kustomize.LoadVariables(ctx, kubeClient, unstructured.Unstructured{Object: ksObj}) if err != nil { return err } if len(ks.Spec.PostBuild.Substitute) > 0 { for k, v := range ks.Spec.PostBuild.Substitute { // Remove new lines from the values as they are not supported. // Replicates the controller behavior from // https://github.com/fluxcd/pkg/blob/main/kustomize/kustomize_varsub.go finalVars[k] = strings.ReplaceAll(v, "\n", "") } } keys := make([]string, 0, len(finalVars)) for k := range finalVars { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { rootCmd.Println(k + "=" + finalVars[k]) } } if debugKustomizationArgs.showHistory { if len(ks.Status.History) == 0 { ks.Status.History = meta.History{} } history, err := yaml.Marshal(ks.Status.History) if err != nil { return err } rootCmd.Println("# History documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#history") rootCmd.Print(string(history)) return nil } return nil } ================================================ FILE: cmd/flux/debug_kustomization_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestDebugKustomization(t *testing.T) { namespace := allocateNamespace("debug") objectFile := "testdata/debug_kustomization/objects.yaml" tmpl := map[string]string{ "fluxns": namespace, } testEnv.CreateObjectFile(objectFile, tmpl, t) cases := []struct { name string arg string goldenFile string tmpl map[string]string }{ { "debug status", "debug ks test --show-status --show-vars=false", "testdata/debug_kustomization/status.golden.yaml", tmpl, }, { "debug vars", "debug ks test --show-vars --show-status=false", "testdata/debug_kustomization/vars.golden.env", tmpl, }, { "debug vars from", "debug ks test-from --show-vars --show-status=false", "testdata/debug_kustomization/vars-from.golden.env", tmpl, }, { "debug history", "debug ks test-with-history --show-history --show-status=false", "testdata/debug_kustomization/history.golden.yaml", tmpl, }, { "debug history empty", "debug ks test --show-history --show-status=false", "testdata/debug_kustomization/history-empty.golden.yaml", tmpl, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.arg + " -n=" + namespace, assert: assertGoldenTemplateFile(tt.goldenFile, tmpl), } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/delete.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "github.com/fluxcd/flux2/v2/internal/utils" ) var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete sources and resources", Long: `The delete sub-commands delete sources and resources.`, } type deleteFlags struct { silent bool } var deleteArgs deleteFlags func init() { deleteCmd.PersistentFlags().BoolVarP(&deleteArgs.silent, "silent", "s", false, "delete resource without asking for confirmation") rootCmd.AddCommand(deleteCmd) } type deleteCommand struct { apiType object adapter // for getting the value, and later deleting it } func (del deleteCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("%s name is required", del.humanKind) } name := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, del.object.asClientObject()) if err != nil { return err } if !deleteArgs.silent { prompt := promptui.Prompt{ Label: "Are you sure you want to delete this " + del.humanKind, IsConfirm: true, } if _, err := prompt.Run(); err != nil { return fmt.Errorf("aborting") } } logger.Actionf("deleting %s %s in %s namespace", del.humanKind, name, *kubeconfigArgs.Namespace) err = kubeClient.Delete(ctx, del.object.asClientObject()) if err != nil { return err } logger.Successf("%s deleted", del.humanKind) return nil } ================================================ FILE: cmd/flux/delete_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var deleteAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Delete a Alert resource", Long: withPreviewNote("The delete alert command removes the given Alert from the cluster."), Example: ` # Delete an Alert and the Kubernetes resources created by it flux delete alert main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: deleteCommand{ apiType: alertType, object: universalAdapter{¬ificationv1.Alert{}}, }.run, } func init() { deleteCmd.AddCommand(deleteAlertCmd) } ================================================ FILE: cmd/flux/delete_alertprovider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var deleteAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Delete a Provider resource", Long: withPreviewNote("The delete alert-provider command removes the given Provider from the cluster."), Example: ` # Delete a Provider and the Kubernetes resources created by it flux delete alert-provider slack`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), RunE: deleteCommand{ apiType: alertProviderType, object: universalAdapter{¬ificationv1.Provider{}}, }.run, } func init() { deleteCmd.AddCommand(deleteAlertProviderCmd) } ================================================ FILE: cmd/flux/delete_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var deleteHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Delete a HelmRelease resource", Long: "The delete helmrelease command removes the given HelmRelease from the cluster.", Example: ` # Delete a Helm release and the Kubernetes resources created by it flux delete hr podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: deleteCommand{ apiType: helmReleaseType, object: universalAdapter{&helmv2.HelmRelease{}}, }.run, } func init() { deleteCmd.AddCommand(deleteHelmReleaseCmd) } ================================================ FILE: cmd/flux/delete_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var deleteImageCmd = &cobra.Command{ Use: "image", Short: "Delete image automation objects", Long: `The delete image sub-commands delete image automation objects.`, } func init() { deleteCmd.AddCommand(deleteImageCmd) } ================================================ FILE: cmd/flux/delete_image_policy.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var deleteImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Delete an ImagePolicy object", Long: `The delete image policy command deletes the given ImagePolicy from the cluster.`, Example: ` # Delete an image policy flux delete image policy alpine3.x`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: deleteCommand{ apiType: imagePolicyType, object: universalAdapter{&imagev1.ImagePolicy{}}, }.run, } func init() { deleteImageCmd.AddCommand(deleteImagePolicyCmd) } ================================================ FILE: cmd/flux/delete_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var deleteImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Delete an ImageRepository object", Long: "The delete image repository command deletes the given ImageRepository from the cluster.", Example: ` # Delete an image repository flux delete image repository alpine`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: deleteCommand{ apiType: imageRepositoryType, object: universalAdapter{&imagev1.ImageRepository{}}, }.run, } func init() { deleteImageCmd.AddCommand(deleteImageRepositoryCmd) } ================================================ FILE: cmd/flux/delete_image_update.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" autov1 "github.com/fluxcd/image-automation-controller/api/v1" ) var deleteImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Delete an ImageUpdateAutomation object", Long: `The delete image update command deletes the given ImageUpdateAutomation from the cluster.`, Example: ` # Delete an image update automation flux delete image update latest-images`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: deleteCommand{ apiType: imageUpdateAutomationType, object: universalAdapter{&autov1.ImageUpdateAutomation{}}, }.run, } func init() { deleteImageCmd.AddCommand(deleteImageUpdateCmd) } ================================================ FILE: cmd/flux/delete_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var deleteKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Delete a Kustomization resource", Long: `The delete kustomization command deletes the given Kustomization from the cluster.`, Example: ` # Delete a kustomization and the Kubernetes resources created by it when prune is enabled flux delete kustomization podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: deleteCommand{ apiType: kustomizationType, object: universalAdapter{&kustomizev1.Kustomization{}}, }.run, } func init() { deleteCmd.AddCommand(deleteKsCmd) } ================================================ FILE: cmd/flux/delete_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var deleteReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Delete a Receiver resource", Long: `The delete receiver command removes the given Receiver from the cluster.`, Example: ` # Delete an Receiver and the Kubernetes resources created by it flux delete receiver main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: deleteCommand{ apiType: receiverType, object: universalAdapter{¬ificationv1.Receiver{}}, }.run, } func init() { deleteCmd.AddCommand(deleteReceiverCmd) } ================================================ FILE: cmd/flux/delete_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var deleteSourceCmd = &cobra.Command{ Use: "source", Short: "Delete sources", Long: `The delete source sub-commands delete sources.`, } func init() { deleteCmd.AddCommand(deleteSourceCmd) } ================================================ FILE: cmd/flux/delete_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Delete a Bucket source", Long: "The delete source bucket command deletes the given Bucket from the cluster.", Example: ` # Delete a Bucket source flux delete source bucket podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: deleteCommand{ apiType: bucketType, object: universalAdapter{&sourcev1.Bucket{}}, }.run, } func init() { deleteSourceCmd.AddCommand(deleteSourceBucketCmd) } ================================================ FILE: cmd/flux/delete_source_chart.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Delete a HelmChart source", Long: "The delete source chart command deletes the given HelmChart from the cluster.", Example: ` # Delete a HelmChart flux delete source chart podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: deleteCommand{ apiType: helmChartType, object: universalAdapter{&sourcev1.HelmChart{}}, }.run, } func init() { deleteSourceCmd.AddCommand(deleteSourceChartCmd) } ================================================ FILE: cmd/flux/delete_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Delete a GitRepository source", Long: `The delete source git command deletes the given GitRepository from the cluster.`, Example: ` # Delete a Git repository flux delete source git podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: deleteCommand{ apiType: gitRepositoryType, object: universalAdapter{&sourcev1.GitRepository{}}, }.run, } func init() { deleteSourceCmd.AddCommand(deleteSourceGitCmd) } ================================================ FILE: cmd/flux/delete_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Delete a HelmRepository source", Long: "The delete source helm command deletes the given HelmRepository from the cluster.", Example: ` # Delete a Helm repository flux delete source helm podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: deleteCommand{ apiType: helmRepositoryType, object: universalAdapter{&sourcev1.HelmRepository{}}, }.run, } func init() { deleteSourceCmd.AddCommand(deleteSourceHelmCmd) } ================================================ FILE: cmd/flux/delete_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Delete an OCIRepository source", Long: withPreviewNote("The delete source oci command deletes the given OCIRepository from the cluster."), Example: ` # Delete an OCIRepository flux delete source oci podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: deleteCommand{ apiType: ociRepositoryType, object: universalAdapter{&sourcev1.OCIRepository{}}, }.run, } func init() { deleteSourceCmd.AddCommand(deleteSourceOCIRepositoryCmd) } ================================================ FILE: cmd/flux/diff.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) 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 prints the diff.`, } func init() { rootCmd.AddCommand(diffCmd) } ================================================ FILE: cmd/flux/diff_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/google/go-containerregistry/pkg/crane" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" ) var diffArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Diff Artifact", Long: `The diff artifact command computes the diff between the remote OCI artifact and a local directory or file`, Example: `# Check if local files differ from remote flux diff artifact oci://ghcr.io/stefanprodan/manifests:podinfo:6.2.0 --path=./kustomize`, RunE: diffArtifactCmdRun, } type diffArtifactFlags struct { path string creds string provider flags.SourceOCIProvider ignorePaths []string insecure bool } var diffArtifactArgs = newDiffArtifactArgs() func newDiffArtifactArgs() diffArtifactFlags { return diffArtifactFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } func init() { diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.path, "path", "", "path to the directory where the Kubernetes manifests are located") diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") diffArtifactCmd.Flags().Var(&diffArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) diffArtifactCmd.Flags().StringSliceVar(&diffArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") diffArtifactCmd.Flags().BoolVar(&diffArtifactArgs.insecure, "insecure-registry", false, "allows the remote artifact to be pulled without TLS") diffCmd.AddCommand(diffArtifactCmd) } func diffArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact URL is required") } ociURL := args[0] if diffArtifactArgs.path == "" { return fmt.Errorf("invalid path %q", diffArtifactArgs.path) } url, err := oci.ParseArtifactURL(ociURL) if err != nil { return err } if _, err := os.Stat(diffArtifactArgs.path); err != nil { return fmt.Errorf("invalid path '%s', must point to an existing directory or file", diffArtifactArgs.path) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() opts := oci.DefaultOptions() if diffArtifactArgs.insecure { opts = append(opts, crane.Insecure) } if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { logger.Actionf("logging in to registry with provider credentials") opt, _, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String()) if err != nil { return fmt.Errorf("error during login with provider: %w", err) } opts = append(opts, opt) } ociClient := oci.NewClient(opts) if diffArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && diffArtifactArgs.creds != "" { logger.Actionf("logging in to registry with credentials") if err := ociClient.LoginWithCredentials(diffArtifactArgs.creds); err != nil { return fmt.Errorf("could not login with credentials: %w", err) } } if err := ociClient.Diff(ctx, url, diffArtifactArgs.path, diffArtifactArgs.ignorePaths); err != nil { return err } logger.Successf("no changes detected") return nil } ================================================ FILE: cmd/flux/diff_artifact_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "testing" "time" "github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" "github.com/phayes/freeport" ctrl "sigs.k8s.io/controller-runtime" ) var dockerReg string func setupRegistryServer(ctx context.Context) error { // Registry config config := &configuration.Configuration{} port, err := freeport.GetFreePort() if err != nil { return fmt.Errorf("failed to get free port: %s", err) } dockerReg = fmt.Sprintf("localhost:%d", port) config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} dockerRegistry, err := registry.NewRegistry(ctx, config) if err != nil { return fmt.Errorf("failed to create docker registry: %w", err) } // Start Docker registry go dockerRegistry.ListenAndServe() return nil } func TestDiffArtifact(t *testing.T) { tests := []struct { name string url string argsTpl string pushFile string diffFile string assert assertFunc }{ { name: "should not fail if there is no diff", url: "oci://%s/podinfo:1.0.0", argsTpl: "diff artifact %s --path=%s", pushFile: "./testdata/diff-artifact/deployment.yaml", diffFile: "./testdata/diff-artifact/deployment.yaml", assert: assertGoldenFile("testdata/diff-artifact/success.golden"), }, { name: "should fail if there is a diff", url: "oci://%s/podinfo:2.0.0", argsTpl: "diff artifact %s --path=%s", pushFile: "./testdata/diff-artifact/deployment.yaml", diffFile: "./testdata/diff-artifact/deployment-diff.yaml", assert: assertError("the remote artifact contents differs from the local one"), }, } ctx := ctrl.SetupSignalHandler() err := setupRegistryServer(ctx) if err != nil { panic(fmt.Sprintf("failed to start docker registry: %s", err)) } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.url = fmt.Sprintf(tt.url, dockerReg) _, err := executeCommand("push artifact " + tt.url + " --path=" + tt.pushFile + " --source=test --revision=test") if err != nil { t.Fatal(fmt.Errorf("failed to push image: %w", err).Error()) } cmd := cmdTestCase{ args: fmt.Sprintf(tt.argsTpl, tt.url, tt.diffFile), assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/diff_kustomization.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "os/signal" "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/build" ) 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 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 # Preview using a local flux kustomization file flux diff kustomization my-app --path ./path/to/local/manifests \ --kustomization-file ./path/to/local/my-app.yaml # Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. flux diff kustomization my-app --path ./path/to/local/manifests \ --kustomization-file ./path/to/local/my-app.yaml \ --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml" # Run recursively on all encountered Kustomizations flux diff kustomization my-app --path ./path/to/local/manifests \ --recursive \ --local-sources GitRepository/flux-system/my-repo=./path/to/local/git`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: diffKsCmdRun, } type diffKsFlags struct { kustomizationFile string path string ignorePaths []string progressBar bool strictSubst bool recursive bool localSources map[string]string } var diffKsArgs diffKsFlags func init() { diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.") diffKsCmd.Flags().BoolVar(&diffKsArgs.progressBar, "progress-bar", true, "Boolean to set the progress bar. The default value is true.") diffKsCmd.Flags().StringSliceVar(&diffKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false, "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations") diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path") diffCmd.AddCommand(diffKsCmd) } func diffKsCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("%s name is required", kustomizationType.humanKind) } name := args[0] if 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 &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} } if diffKsArgs.kustomizationFile != "" { if fs, err := os.Stat(diffKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { return fmt.Errorf("invalid kustomization file %q", diffKsArgs.kustomizationFile) } } var ( builder *build.Builder err error ) if diffKsArgs.progressBar { builder, err = build.NewBuilder(name, diffKsArgs.path, build.WithClientConfig(kubeconfigArgs, kubeclientOptions), build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithProgressBar(), build.WithIgnore(diffKsArgs.ignorePaths), build.WithStrictSubstitute(diffKsArgs.strictSubst), build.WithRecursive(diffKsArgs.recursive), build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), ) } else { builder, err = build.NewBuilder(name, diffKsArgs.path, build.WithClientConfig(kubeconfigArgs, kubeclientOptions), build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithIgnore(diffKsArgs.ignorePaths), build.WithStrictSubstitute(diffKsArgs.strictSubst), build.WithRecursive(diffKsArgs.recursive), build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), ) } if err != nil { return &RequestError{StatusCode: 2, Err: err} } // create a signal channel sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt) errChan := make(chan error) go func() { output, hasChanged, err := builder.Diff() if err != nil { errChan <- &RequestError{StatusCode: 2, Err: err} } cmd.Print(output) if hasChanged { errChan <- &RequestError{StatusCode: 1, Err: fmt.Errorf("identified at least one change, exiting with non-zero exit code")} } else { errChan <- nil } }() select { case <-sigc: if diffKsArgs.progressBar { err := builder.StopSpinner() if err != nil { return err } } fmt.Println("Build cancelled... exiting.") return builder.Cancel() case err := <-errChan: if err != nil { return err } } return nil } ================================================ FILE: cmd/flux/diff_kustomization_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "os" "strings" "testing" "github.com/fluxcd/flux2/v2/internal/build" "github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa/normalize" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestDiffKustomization(t *testing.T) { tests := []struct { name string args string objectFile string assert assertFunc }{ { name: "no args", args: "diff kustomization podinfo", objectFile: "", assert: assertError("invalid resource path \"\""), }, { name: "diff nothing deployed", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "", assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), }, { name: "diff with a deployment object", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/deployment.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-deployment.golden"), }, { name: "diff with a drifted service object", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/service.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-service.golden"), }, { name: "diff with a drifted secret object", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-secret.golden"), }, { name: "diff with a drifted key in sops secret object", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/key-sops-secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden"), }, { name: "diff with a drifted value in sops secret object", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", 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 --progress-bar=false", 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 --progress-bar=false", objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden"), }, { name: "diff where kustomization file has multiple objects with the same name", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml", objectFile: "", assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), }, { name: "diff with recursive", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --progress-bar=false --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization", objectFile: "./testdata/diff-kustomization/my-app.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-recursive.golden"), }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } b, _ := build.NewBuilder("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions)) resourceManager, err := b.Manager() if err != nil { t.Fatal(err) } setup(t, tmpl) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.objectFile != "" { if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile(tt.objectFile, tmpl, t), ssa.DefaultApplyOptions()); err != nil { t.Error(err) } } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], assert: tt.assert, } cmd.runTestCmd(t) if tt.objectFile != "" { testEnv.DeleteObjectFile(tt.objectFile, tmpl, t) } }) } } func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { buf, err := os.ReadFile(objectFile) if err != nil { t.Fatalf("Error reading file '%s': %v", objectFile, err) } content, err := executeTemplate(string(buf), templateValues) if err != nil { t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) } clientObjects, err := readYamlObjects(strings.NewReader(content)) if err != nil { t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) } if err := normalize.UnstructuredList(clientObjects); err != nil { t.Fatalf("Error setting native kinds defaults for '%s': %v", objectFile, err) } return clientObjects } ================================================ FILE: cmd/flux/docgen.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "path" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) const fmTemplate = `--- title: "%s" --- ` var ( cmdDocPath string ) var docgenCmd = &cobra.Command{ Use: "docgen", Short: "Generate the documentation for the CLI commands.", Hidden: true, RunE: docgenCmdRun, } func init() { docgenCmd.Flags().StringVar(&cmdDocPath, "path", "./docs/cmd", "path to write the generated documentation to") rootCmd.AddCommand(docgenCmd) } func docgenCmdRun(cmd *cobra.Command, args []string) error { err := doc.GenMarkdownTreeCustom(rootCmd, cmdDocPath, frontmatterPrepender, linkHandler) if err != nil { return err } return nil } func frontmatterPrepender(filename string) string { name := filepath.Base(filename) base := strings.TrimSuffix(name, path.Ext(name)) title := strings.Replace(base, "_", " ", -1) return fmt.Sprintf(fmTemplate, title) } func linkHandler(name string) string { base := strings.TrimSuffix(name, path.Ext(name)) return "../" + strings.ToLower(base) + "/" } ================================================ FILE: cmd/flux/envsubst.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "fmt" "github.com/fluxcd/pkg/envsubst" "github.com/spf13/cobra" ) var envsubstCmd = &cobra.Command{ Use: "envsubst", Args: cobra.NoArgs, Short: "envsubst substitutes the values of environment variables", Long: withPreviewNote(`The envsubst command substitutes the values of environment variables in the string piped as standard input and writes the result to the standard output. This command can be used to replicate the behavior of the Flux Kustomization post-build substitutions.`), Example: ` # Run env var substitutions on the kustomization build output export cluster_region=eu-central-1 kustomize build . | flux envsubst # Run env var substitutions and error out if a variable is not set kustomize build . | flux envsubst --strict `, RunE: runEnvsubstCmd, } type envsubstFlags struct { strict bool } var envsubstArgs envsubstFlags func init() { envsubstCmd.Flags().BoolVar(&envsubstArgs.strict, "strict", false, "fail if a variable without a default value is declared in the input but is missing from the environment") rootCmd.AddCommand(envsubstCmd) } func runEnvsubstCmd(cmd *cobra.Command, args []string) error { stdin := bufio.NewScanner(rootCmd.InOrStdin()) stdout := bufio.NewWriter(rootCmd.OutOrStdout()) for stdin.Scan() { line, err := envsubst.EvalEnv(stdin.Text(), envsubstArgs.strict) if err != nil { return err } _, err = fmt.Fprintln(stdout, line) if err != nil { return err } err = stdout.Flush() if err != nil { return err } } return nil } ================================================ FILE: cmd/flux/envsubst_test.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "os" "testing" . "github.com/onsi/gomega" ) func TestEnvsubst(t *testing.T) { g := NewWithT(t) input, err := os.ReadFile("testdata/envsubst/file.yaml") g.Expect(err).NotTo(HaveOccurred()) t.Setenv("REPO_NAME", "test") output, err := executeCommandWithIn("envsubst", bytes.NewReader(input)) g.Expect(err).NotTo(HaveOccurred()) expected, err := os.ReadFile("testdata/envsubst/file.gold") g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal(string(expected))) } func TestEnvsubst_Strinct(t *testing.T) { g := NewWithT(t) input, err := os.ReadFile("testdata/envsubst/file.yaml") g.Expect(err).NotTo(HaveOccurred()) _, err = executeCommandWithIn("envsubst --strict", bytes.NewReader(input)) g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("variable not set (strict mode)")) } ================================================ FILE: cmd/flux/events.go ================================================ /* Copyright 2023 The Kubernetes Authors. Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "sort" "strings" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/watch" runtimeresource "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" autov1 "github.com/fluxcd/image-automation-controller/api/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" sourcev1 "github.com/fluxcd/source-controller/api/v1" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/printers" ) var eventsCmd = &cobra.Command{ Use: "events", Short: "Display Kubernetes events for Flux resources", Long: withPreviewNote("The events sub-command shows Kubernetes events from Flux resources"), Example: ` # Display events for flux resources in default namespace flux events -n default # Display events for flux resources in all namespaces flux events -A # Display events for a Kustomization named podinfo flux events --for Kustomization/podinfo # Display events for all Kustomizations in default namespace flux events --for Kustomization -n default # Display warning events for alert resources flux events --for Alert/podinfo --types warning `, RunE: eventsCmdRun, } type eventFlags struct { allNamespaces bool watch bool forSelector string filterTypes []string } var eventArgs eventFlags func init() { eventsCmd.Flags().BoolVarP(&eventArgs.allNamespaces, "all-namespaces", "A", false, "display events from Flux resources across all namespaces") eventsCmd.Flags().BoolVarP(&eventArgs.watch, "watch", "w", false, "indicate if the events should be streamed") eventsCmd.Flags().StringVar(&eventArgs.forSelector, "for", "", "get events for a particular object") eventsCmd.Flags().StringSliceVar(&eventArgs.filterTypes, "types", []string{}, "filter events for certain types (valid types are: Normal, Warning)") rootCmd.AddCommand(eventsCmd) } func eventsCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() if err := validateEventTypes(eventArgs.filterTypes); err != nil { return err } kubeclient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespace := *kubeconfigArgs.Namespace if eventArgs.allNamespaces { namespace = "" } var diffRefNs bool // Build the base list options. When --all-namespaces is set we must NOT constrain the // query to a single namespace, otherwise we silently return a partial result set. clientListOpts := []client.ListOption{} if !eventArgs.allNamespaces { clientListOpts = append(clientListOpts, client.InNamespace(*kubeconfigArgs.Namespace)) } var refListOpts [][]client.ListOption if eventArgs.forSelector != "" { kind, name := getKindNameFromSelector(eventArgs.forSelector) if kind == "" { return fmt.Errorf("--for selector must be of format [/]") } refInfoKind, err := fluxKindMap.getRefInfo(kind) if err != nil { return err } clientListOpts = append(clientListOpts, getListOpt(refInfoKind.gvk.Kind, name)) if name != "" { refs, err := getObjectRef(ctx, kubeclient, refInfoKind, name, *kubeconfigArgs.Namespace) if err != nil { return err } for _, ref := range refs { refKind, refName, refNs := utils.ParseObjectKindNameNamespace(ref) if refNs != namespace { diffRefNs = true } refOpt := []client.ListOption{getListOpt(refKind, refName), client.InNamespace(refNs)} refListOpts = append(refListOpts, refOpt) } } } showNamespace := namespace == "" || diffRefNs if eventArgs.watch { return eventsCmdWatchRun(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) } rows, err := getRows(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) if err != nil { return err } if len(rows) == 0 { if eventArgs.allNamespaces { logger.Failuref("No events found.") } else { logger.Failuref("No events found in %s namespace.", *kubeconfigArgs.Namespace) } return nil } headers := getHeaders(showNamespace) return printers.TablePrinter(headers).Print(cmd.OutOrStdout(), rows) } func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) ([][]string, error) { el := &corev1.EventList{} if err := addEventsToList(ctx, kubeclient, el, clientListOpts); err != nil { return nil, err } for _, refOpts := range refListOpts { if err := addEventsToList(ctx, kubeclient, el, refOpts); err != nil { return nil, err } } sort.Sort(SortableEvents(el.Items)) var rows [][]string for _, item := range el.Items { if ignoreEvent(item) { continue } rows = append(rows, getEventRow(item, showNs)) } return rows, nil } func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error { listOpts := &metav1.ListOptions{} err := runtimeresource.FollowContinue(listOpts, func(options metav1.ListOptions) (runtime.Object, error) { newEvents := &corev1.EventList{} opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize)) if options.Continue != "" { opts = append(opts, client.Continue(options.Continue)) } if err := kubeclient.List(ctx, newEvents, opts...); err != nil { return nil, fmt.Errorf("error getting events: %w", err) } el.Items = append(el.Items, newEvents.Items...) return newEvents, nil }) return err } func getListOpt(kind, name string) client.ListOption { var sel fields.Selector if name == "" { sel = fields.OneTermEqualSelector("involvedObject.kind", kind) } else { sel = fields.AndSelectors( fields.OneTermEqualSelector("involvedObject.kind", kind), fields.OneTermEqualSelector("involvedObject.name", name)) } return client.MatchingFieldsSelector{Selector: sel} } func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) error { event := &corev1.EventList{} listOpts = append(listOpts, client.Limit(cmdutil.DefaultChunkSize)) eventWatch, err := kubeclient.Watch(ctx, event, listOpts...) if err != nil { return err } firstIteration := true handleEvent := func(e watch.Event) error { if e.Type == watch.Deleted { return nil } event, ok := e.Object.(*corev1.Event) if !ok { return nil } if ignoreEvent(*event) { return nil } rows := getEventRow(*event, showNs) var hdr []string if firstIteration { hdr = getHeaders(showNs) firstIteration = false } return printers.TablePrinter(hdr).Print(rootCmd.OutOrStdout(), [][]string{rows}) } for _, refOpts := range refListOpts { refEventWatch, err := kubeclient.Watch(ctx, event, refOpts...) if err != nil { return err } go func() { if err := receiveEventChan(ctx, refEventWatch, handleEvent); err != nil { logger.Failuref("error watching events: %s", err.Error()) } }() } return receiveEventChan(ctx, eventWatch, handleEvent) } func receiveEventChan(ctx context.Context, eventWatch watch.Interface, f func(e watch.Event) error) error { defer eventWatch.Stop() for { select { case e, ok := <-eventWatch.ResultChan(): if !ok { return nil } err := f(e) if err != nil { return err } case <-ctx.Done(): return nil } } } func getHeaders(showNs bool) []string { headers := []string{"Last seen", "Type", "Reason", "Object", "Message"} if showNs { headers = append(namespaceHeader, headers...) } return headers } func getEventRow(e corev1.Event, showNs bool) []string { var row []string if showNs { row = []string{e.Namespace} } row = append(row, getLastSeen(e), e.Type, e.Reason, fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), e.Message) return row } // getObjectRef is used to get the metadata of a resource that the selector(in the format ) references. // It returns an empty string if the resource doesn't reference any resource // and a string with the format `/.` if it does. func getObjectRef(ctx context.Context, kubeclient client.Client, ref refInfo, name, ns string) ([]string, error) { // the resource has no source ref if len(ref.field) == 0 { return nil, nil } obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(schema.GroupVersionKind{ Kind: ref.gvk.Kind, Version: ref.gvk.Version, Group: ref.gvk.Group, }) objName := types.NamespacedName{ Namespace: ns, Name: name, } if err := kubeclient.Get(ctx, objName, obj); err != nil { return nil, err } refKind := ref.kind if refKind == "" { kindField := append(ref.field, "kind") specKind, ok, err := unstructured.NestedString(obj.Object, kindField...) if err != nil { return nil, err } if !ok { return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(kindField, "."), objName) } refKind = specKind } nameField := append(ref.field, "name") refName, ok, err := unstructured.NestedString(obj.Object, nameField...) if err != nil { return nil, err } if !ok { return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(nameField, "."), objName) } var allRefs []string refNamespace := ns if ref.crossNamespaced { namespaceField := append(ref.field, "namespace") namespace, ok, err := unstructured.NestedString(obj.Object, namespaceField...) if err != nil { return nil, err } if ok { refNamespace = namespace } } allRefs = append(allRefs, fmt.Sprintf("%s/%s.%s", refKind, refName, refNamespace)) if ref.otherRefs != nil { for _, otherRef := range ref.otherRefs(ns, name) { allRefs = append(allRefs, fmt.Sprintf("%s.%s", otherRef, refNamespace)) } } return allRefs, nil } type refMap map[string]refInfo func (r refMap) getRefInfo(kind string) (refInfo, error) { for key, ref := range r { if strings.EqualFold(key, kind) { return ref, nil } } return refInfo{}, fmt.Errorf("'%s' is not a recognized Flux kind", kind) } func (r refMap) hasKind(kind string) bool { _, err := r.getRefInfo(kind) return err == nil } // validateEventTypes checks that the event types passed into the function // is either equal to `Normal` or `Warning` which are currently the two supported types. // https://github.com/kubernetes/kubernetes/blob/a8a1abc25cad87333840cd7d54be2efaf31a3177/staging/src/k8s.io/api/core/v1/types.go#L6212 func validateEventTypes(eventTypes []string) error { for _, t := range eventTypes { if !strings.EqualFold(corev1.EventTypeWarning, t) && !strings.EqualFold(corev1.EventTypeNormal, t) { return fmt.Errorf("type '%s' not supported. Supported types are Normal, Warning", t) } } return nil } type refInfo struct { // gvk is the group version kind of the resource gvk schema.GroupVersionKind // kind is the kind that the resource references if it's not static kind string // crossNamespaced indicates if this resource uses cross namespaced references crossNamespaced bool // otherRefs returns other reference that might not be directly accessible // from the spec of the object otherRefs func(namespace, name string) []string field []string } var fluxKindMap = refMap{ kustomizev1.KustomizationKind: { gvk: kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind), crossNamespaced: true, field: []string{"spec", "sourceRef"}, }, helmv2.HelmReleaseKind: { gvk: helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind), crossNamespaced: true, otherRefs: func(namespace, name string) []string { return []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, namespace, name)} }, field: []string{"spec", "chart", "spec", "sourceRef"}, }, notificationv1b3.AlertKind: { gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.AlertKind), kind: notificationv1b3.ProviderKind, crossNamespaced: false, field: []string{"spec", "providerRef"}, }, notificationv1.ReceiverKind: {gvk: notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)}, notificationv1b3.ProviderKind: {gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.ProviderKind)}, imagev1.ImagePolicyKind: { gvk: imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind), kind: imagev1.ImageRepositoryKind, crossNamespaced: true, field: []string{"spec", "imageRepositoryRef"}, }, sourcev1.HelmChartKind: { gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind), crossNamespaced: true, field: []string{"spec", "sourceRef"}, }, sourcev1.GitRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)}, sourcev1.OCIRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)}, sourcev1.BucketKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)}, sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)}, autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)}, imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)}, swapi.ArtifactGeneratorKind: {gvk: swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)}, } func ignoreEvent(e corev1.Event) bool { if !fluxKindMap.hasKind(e.InvolvedObject.Kind) { return true } if len(eventArgs.filterTypes) > 0 { _, equal := utils.ContainsEqualFoldItemString(eventArgs.filterTypes, e.Type) if !equal { return true } } return false } func getKindNameFromSelector(selector string) (string, string) { kind, name := utils.ParseObjectKindName(selector) // if there's no slash in the selector utils.ParseObjectKindName returns the // input string as the name but here we want it as the kind instead if kind == "" && name != "" { kind = name name = "" } return kind, name } // The functions below are copied from: https://github.com/kubernetes/kubectl/blob/4ecd7bd0f0799f191335a331ca3c6a397a888233/pkg/cmd/events/events.go#L294 // SortableEvents implements sort.Interface for []api.Event by time type SortableEvents []corev1.Event func (list SortableEvents) Len() int { return len(list) } func (list SortableEvents) Swap(i, j int) { list[i], list[j] = list[j], list[i] } // Return the time that should be used for sorting, which can come from // various places in corev1.Event. func eventTime(event corev1.Event) time.Time { if event.Series != nil { return event.Series.LastObservedTime.Time } if !event.LastTimestamp.Time.IsZero() { return event.LastTimestamp.Time } return event.EventTime.Time } func (list SortableEvents) Less(i, j int) bool { return eventTime(list[i]).Before(eventTime(list[j])) } func getLastSeen(e corev1.Event) string { var interval string firstTimestampSince := translateMicroTimestampSince(e.EventTime) if e.EventTime.IsZero() { firstTimestampSince = translateTimestampSince(e.FirstTimestamp) } if e.Series != nil { interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince) } else if e.Count > 1 { interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince) } else { interval = firstTimestampSince } return interval } // translateMicroTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateMicroTimestampSince(timestamp metav1.MicroTime) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } // translateTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateTimestampSince(timestamp metav1.Time) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } ================================================ FILE: cmd/flux/events_test.go ================================================ /* Copyright 2023 The Kubernetes Authors. Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "strconv" "strings" "testing" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/internal/utils" ) var objects = ` apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system namespace: flux-system spec: interval: 5m0s path: ./infrastructure/ prune: true sourceRef: kind: GitRepository name: flux-system --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: default spec: interval: 5m0s path: ./infrastructure/ prune: true sourceRef: kind: GitRepository name: flux-system namespace: flux-system --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: flux-system namespace: flux-system spec: interval: 5m0s ref: branch: main secretRef: name: flux-system timeout: 1m0s url: ssh://git@github.com/example/repo --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: default spec: chart: spec: chart: podinfo reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo namespace: flux-system version: '*' interval: 5m0s --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s url: https://stefanprodan.github.io/podinfo --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: default-podinfo namespace: flux-system spec: chart: podinfo interval: 1m0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo-chart version: '*' --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: webapp namespace: flux-system spec: eventSeverity: info eventSources: - kind: GitRepository name: '*' providerRef: name: slack --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: slack namespace: flux-system spec: address: https://hooks.slack.com/services/mock type: slack --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy metadata: name: podinfo namespace: default spec: imageRepositoryRef: name: acr-podinfo namespace: flux-system policy: semver: range: 5.0.x --- apiVersion: v1 kind: Namespace metadata: name: flux-system` func Test_getObjectRef(t *testing.T) { g := NewWithT(t) objs, err := ssautil.ReadObjects(strings.NewReader(objects)) g.Expect(err).To(Not(HaveOccurred())) builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) for _, obj := range objs { builder = builder.WithObjects(obj) } c := builder.Build() tests := []struct { name string selector string namespace string want []string wantErr bool }{ { name: "Source Ref for Kustomization", selector: "Kustomization/flux-system", namespace: "flux-system", want: []string{"GitRepository/flux-system.flux-system"}, }, { name: "Crossnamespace Source Ref for Kustomization", selector: "Kustomization/podinfo", namespace: "default", want: []string{"GitRepository/flux-system.flux-system"}, }, { name: "Source Ref for HelmRelease", selector: "HelmRelease/podinfo", namespace: "default", want: []string{"HelmRepository/podinfo.flux-system", "HelmChart/default-podinfo.flux-system"}, }, { name: "Source Ref for Alert", selector: "Alert/webapp", namespace: "flux-system", want: []string{"Provider/slack.flux-system"}, }, { name: "Source Ref for ImagePolicy", selector: "ImagePolicy/podinfo", namespace: "default", want: []string{"ImageRepository/acr-podinfo.flux-system"}, }, { name: "Source Ref for ImagePolicy (lowercased)", selector: "imagepolicy/podinfo", namespace: "default", want: []string{"ImageRepository/acr-podinfo.flux-system"}, }, { name: "Empty Ref for Provider", selector: "Provider/slack", namespace: "flux-system", want: nil, }, { name: "Non flux resource", selector: "Namespace/flux-system", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) kind, name := getKindNameFromSelector(tt.selector) infoRef, err := fluxKindMap.getRefInfo(kind) if tt.wantErr { g.Expect(err).To(HaveOccurred()) return } got, err := getObjectRef(context.Background(), c, infoRef, name, tt.namespace) g.Expect(err).To(Not(HaveOccurred())) g.Expect(got).To(Equal(tt.want)) }) } } func Test_getRows(t *testing.T) { g := NewWithT(t) objs, err := ssautil.ReadObjects(strings.NewReader(objects)) g.Expect(err).To(Not(HaveOccurred())) builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) for _, obj := range objs { builder = builder.WithObjects(obj) } eventList := &corev1.EventList{} for _, obj := range objs { infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason") warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason") eventList.Items = append(eventList.Items, infoEvent, warningEvent) } builder = builder.WithLists(eventList) builder.WithIndex(&corev1.Event{}, "involvedObject.kind/name", kindNameIndexer) builder.WithIndex(&corev1.Event{}, "involvedObject.kind", kindIndexer) c := builder.Build() tests := []struct { name string selector string refSelector string namespace string refNs string expected [][]string }{ { name: "events from all namespaces", selector: "", namespace: "", expected: [][]string{ {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, {"default", "", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "Alert/webapp", "Error Message"}, {"flux-system", "", "info", "Info Reason", "Alert/webapp", "Info Message"}, {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "Kustomization/flux-system", "Error Message"}, {"flux-system", "", "info", "Info Reason", "Kustomization/flux-system", "Info Message"}, {"flux-system", "", "error", "Error Reason", "Provider/slack", "Error Message"}, {"flux-system", "", "info", "Info Reason", "Provider/slack", "Info Message"}, }, }, { name: "events from default namespaces", selector: "", namespace: "default", expected: [][]string{ {"", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, {"", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, {"", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, {"", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, {"", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, {"", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, }, }, { name: "Kustomization with crossnamespaced GitRepository", selector: "Kustomization/podinfo", namespace: "default", expected: [][]string{ {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, }, }, { name: "All Kustomization (lowercased selector)", selector: "kustomization", expected: [][]string{ {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "Kustomization/flux-system", "Error Message"}, {"flux-system", "", "info", "Info Reason", "Kustomization/flux-system", "Info Message"}, }, }, { name: "HelmRelease with crossnamespaced HelmRepository", selector: "HelmRelease/podinfo", namespace: "default", expected: [][]string{ {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, }, }, { name: "HelmRelease with crossnamespaced HelmRepository (lowercased)", selector: "helmrelease/podinfo", namespace: "default", expected: [][]string{ {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) var refs []string var refNs, refKind, refName string var clientOpts = []client.ListOption{client.InNamespace(tt.namespace)} if tt.selector != "" { kind, name := getKindNameFromSelector(tt.selector) infoRef, err := fluxKindMap.getRefInfo(kind) clientOpts = append(clientOpts, getTestListOpt(infoRef.gvk.Kind, name)) if name != "" { g.Expect(err).To(Not(HaveOccurred())) refs, err = getObjectRef(context.Background(), c, infoRef, name, tt.namespace) g.Expect(err).To(Not(HaveOccurred())) } } g.Expect(err).To(Not(HaveOccurred())) var refOpts [][]client.ListOption for _, ref := range refs { refKind, refName, refNs = utils.ParseObjectKindNameNamespace(ref) refOpts = append(refOpts, []client.ListOption{client.InNamespace(refNs), getTestListOpt(refKind, refName)}) } showNs := tt.namespace == "" || (refNs != "" && refNs != tt.namespace) rows, err := getRows(context.Background(), c, clientOpts, refOpts, showNs) g.Expect(err).To(Not(HaveOccurred())) g.Expect(rows).To(ConsistOf(tt.expected)) }) } } func getTestListOpt(kind, name string) client.ListOption { var sel fields.Selector if name == "" { sel = fields.OneTermEqualSelector("involvedObject.kind", kind) } else { sel = fields.OneTermEqualSelector("involvedObject.kind/name", fmt.Sprintf("%s/%s", kind, name)) } return client.MatchingFieldsSelector{Selector: sel} } func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event { return corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Namespace: obj.GetNamespace(), // name of event needs to be unique Name: obj.GetNamespace() + obj.GetNamespace() + obj.GetObjectKind().GroupVersionKind().Kind + eventType, }, Reason: reason, Message: msg, Type: eventType, InvolvedObject: corev1.ObjectReference{ Kind: obj.GetObjectKind().GroupVersionKind().Kind, Namespace: obj.GetNamespace(), Name: obj.GetName(), }, } } // paginatedClient wraps a client.Client and simulates real Kubernetes API // pagination by splitting List results into pages of pageSize items, // using the ListMeta.Continue token. type paginatedClient struct { client.Client pageSize int } func (c *paginatedClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { listOpts := &client.ListOptions{} listOpts.ApplyOptions(opts) // Fetch all results from the underlying client (without Limit/Continue). stripped := make([]client.ListOption, 0, len(opts)) for _, o := range opts { if _, ok := o.(client.Limit); ok { continue } if _, ok := o.(client.Continue); ok { continue } stripped = append(stripped, o) } if err := c.Client.List(ctx, list, stripped...); err != nil { return err } items, err := meta.ExtractList(list) if err != nil { return err } // Determine the page window based on the Continue token. start := 0 if listOpts.Continue != "" { n, err := strconv.Atoi(listOpts.Continue) if err != nil { return fmt.Errorf("invalid continue token: %w", err) } start = n } if start > len(items) { start = len(items) } end := start + c.pageSize if end > len(items) { end = len(items) } page := items[start:end] if err := meta.SetList(list, page); err != nil { return err } // Set the Continue token when there are more pages. listAccessor, err := meta.ListAccessor(list) if err != nil { return err } if end < len(items) { listAccessor.SetContinue(strconv.Itoa(end)) } else { listAccessor.SetContinue("") } return nil } func Test_addEventsToList_pagination(t *testing.T) { g := NewWithT(t) objs, err := ssautil.ReadObjects(strings.NewReader(objects)) g.Expect(err).To(Not(HaveOccurred())) builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) for _, obj := range objs { builder = builder.WithObjects(obj) } eventList := &corev1.EventList{} for _, obj := range objs { infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason") warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason") eventList.Items = append(eventList.Items, infoEvent, warningEvent) } builder = builder.WithLists(eventList) c := builder.Build() totalEvents := len(eventList.Items) g.Expect(totalEvents).To(BeNumerically(">", 2), "need more than 2 events to test pagination") // Wrap the client to paginate at 2 items per page, forcing multiple // round-trips through FollowContinue. pc := &paginatedClient{Client: c, pageSize: 2} el := &corev1.EventList{} err = addEventsToList(context.Background(), pc, el, nil) g.Expect(err).To(Not(HaveOccurred())) g.Expect(el.Items).To(HaveLen(totalEvents), "addEventsToList should collect all events across paginated responses") } func kindNameIndexer(obj client.Object) []string { e, ok := obj.(*corev1.Event) if !ok { panic(fmt.Sprintf("Expected a Event, got %T", e)) } return []string{fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name)} } func kindIndexer(obj client.Object) []string { e, ok := obj.(*corev1.Event) if !ok { panic(fmt.Sprintf("Expected a Event, got %T", e)) } return []string{e.InvolvedObject.Kind} } ================================================ FILE: cmd/flux/export.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "context" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/utils" ) var exportCmd = &cobra.Command{ Use: "export", Short: "Export resources in YAML format", Long: `The export sub-commands export resources in YAML format.`, } type exportFlags struct { all bool } var exportArgs exportFlags func init() { exportCmd.PersistentFlags().BoolVar(&exportArgs.all, "all", false, "select all resources") rootCmd.AddCommand(exportCmd) } // exportable represents a type that you can fetch from the Kubernetes // API, then tidy up for serialising. type exportable interface { adapter export() interface{} } // exportableList represents a type that has a list of values, each of // which is exportable. type exportableList interface { listAdapter exportItem(i int) interface{} } type exportCommand struct { object exportable list exportableList } func (export exportCommand) run(cmd *cobra.Command, args []string) error { if !exportArgs.all && len(args) < 1 { return fmt.Errorf("name is required") } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if exportArgs.all { err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(*kubeconfigArgs.Namespace)) if err != nil { return err } if export.list.len() == 0 { return fmt.Errorf("no objects found in %s namespace", *kubeconfigArgs.Namespace) } for i := 0; i < export.list.len(); i++ { if err = printExport(export.list.exportItem(i)); err != nil { return err } } } else { name := args[0] namespacedName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, export.object.asClientObject()) if err != nil { return err } return printExport(export.object.export()) } return nil } func printExport(export any) error { data, err := yaml.Marshal(export) if err != nil { return err } printlnStdout("---") printlnStdout(resourceToString(data)) return nil } func resourceToString(data []byte) string { data = bytes.Replace(data, []byte(" creationTimestamp: null\n"), []byte(""), 1) data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) data = bytes.TrimSpace(data) return string(data) } ================================================ FILE: cmd/flux/export_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var exportAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Export Alert resources in YAML format", Long: withPreviewNote("The export alert command exports one or all Alert resources in YAML format."), Example: ` # Export all Alert resources flux export alert --all > alerts.yaml # Export a Alert flux export alert main > main.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: exportCommand{ object: alertAdapter{¬ificationv1.Alert{}}, list: alertListAdapter{¬ificationv1.AlertList{}}, }.run, } func init() { exportCmd.AddCommand(exportAlertCmd) } func exportAlert(alert *notificationv1.Alert) interface{} { gvk := notificationv1.GroupVersion.WithKind("Alert") export := notificationv1.Alert{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: alert.Name, Namespace: alert.Namespace, Labels: alert.Labels, Annotations: alert.Annotations, }, Spec: alert.Spec, } return export } func (ex alertAdapter) export() interface{} { return exportAlert(ex.Alert) } func (ex alertListAdapter) exportItem(i int) interface{} { return exportAlert(&ex.AlertList.Items[i]) } ================================================ FILE: cmd/flux/export_alertprovider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var exportAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Export Provider resources in YAML format", Long: withPreviewNote("The export alert-provider command exports one or all Provider resources in YAML format."), Example: ` # Export all Provider resources flux export alert-provider --all > alert-providers.yaml # Export a Provider flux export alert-provider slack > slack.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), RunE: exportCommand{ object: alertProviderAdapter{¬ificationv1.Provider{}}, list: alertProviderListAdapter{¬ificationv1.ProviderList{}}, }.run, } func init() { exportCmd.AddCommand(exportAlertProviderCmd) } func exportAlertProvider(alertProvider *notificationv1.Provider) interface{} { gvk := notificationv1.GroupVersion.WithKind("Provider") export := notificationv1.Provider{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: alertProvider.Name, Namespace: alertProvider.Namespace, Labels: alertProvider.Labels, Annotations: alertProvider.Annotations, }, Spec: alertProvider.Spec, } return export } func (ex alertProviderAdapter) export() interface{} { return exportAlertProvider(ex.Provider) } func (ex alertProviderListAdapter) exportItem(i int) interface{} { return exportAlertProvider(&ex.ProviderList.Items[i]) } ================================================ FILE: cmd/flux/export_artifact.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var exportArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Export artifact objects", Long: `The export artifact sub-commands export artifacts objects in YAML format.`, } func init() { exportCmd.AddCommand(exportArtifactCmd) } ================================================ FILE: cmd/flux/export_artifact_generator.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" ) var exportArtifactGeneratorCmd = &cobra.Command{ Use: "generator [name]", Short: "Export ArtifactGenerator resources in YAML format", Long: "The export artifact generator command exports one or all ArtifactGenerator resources in YAML format.", Example: ` # Export all ArtifactGenerator resources flux export artifact generator --all > artifact-generators.yaml # Export a specific generator flux export artifact generator my-generator > my-generator.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)), RunE: exportCommand{ object: artifactGeneratorAdapter{&swapi.ArtifactGenerator{}}, list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}}, }.run, } func init() { exportArtifactCmd.AddCommand(exportArtifactGeneratorCmd) } // Export returns an ArtifactGenerator value which has // extraneous information stripped out. func exportArtifactGenerator(item *swapi.ArtifactGenerator) interface{} { gvk := swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind) export := swapi.ArtifactGenerator{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: item.Name, Namespace: item.Namespace, Labels: item.Labels, Annotations: item.Annotations, }, Spec: item.Spec, } return export } func (ex artifactGeneratorAdapter) export() interface{} { return exportArtifactGenerator(ex.ArtifactGenerator) } func (ex artifactGeneratorListAdapter) exportItem(i int) interface{} { return exportArtifactGenerator(&ex.ArtifactGeneratorList.Items[i]) } ================================================ FILE: cmd/flux/export_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var exportHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Export HelmRelease resources in YAML format", Long: "The export helmrelease command exports one or all HelmRelease resources in YAML format.", Example: ` # Export all HelmRelease resources flux export helmrelease --all > kustomizations.yaml # Export a HelmRelease flux export hr my-app > app-release.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: exportCommand{ object: helmReleaseAdapter{&helmv2.HelmRelease{}}, list: helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, }.run, } func init() { exportCmd.AddCommand(exportHelmReleaseCmd) } func exportHelmRelease(helmRelease *helmv2.HelmRelease) interface{} { gvk := helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind) export := helmv2.HelmRelease{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: helmRelease.Name, Namespace: helmRelease.Namespace, Labels: helmRelease.Labels, Annotations: helmRelease.Annotations, }, Spec: helmRelease.Spec, } return export } func (ex helmReleaseAdapter) export() interface{} { return exportHelmRelease(ex.HelmRelease) } func (ex helmReleaseListAdapter) exportItem(i int) interface{} { return exportHelmRelease(&ex.HelmReleaseList.Items[i]) } ================================================ FILE: cmd/flux/export_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var exportImageCmd = &cobra.Command{ Use: "image", Short: "Export image automation objects", Long: `The export image sub-commands export image automation objects in YAML format.`, } func init() { exportCmd.AddCommand(exportImageCmd) } ================================================ FILE: cmd/flux/export_image_policy.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var exportImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Export ImagePolicy resources in YAML format", Long: "The export image policy command exports one or all ImagePolicy resources in YAML format.", Example: ` # Export all ImagePolicy resources flux export image policy --all > image-policies.yaml # Export a specific policy flux export image policy alpine1x > alpine1x.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: exportCommand{ object: imagePolicyAdapter{&imagev1.ImagePolicy{}}, list: imagePolicyListAdapter{&imagev1.ImagePolicyList{}}, }.run, } func init() { exportImageCmd.AddCommand(exportImagePolicyCmd) } // Export returns a ImagePolicy value which has extraneous information // stripped out. func exportImagePolicy(item *imagev1.ImagePolicy) interface{} { gvk := imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind) export := imagev1.ImagePolicy{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: item.Name, Namespace: item.Namespace, Labels: item.Labels, Annotations: item.Annotations, }, Spec: item.Spec, } return export } func (ex imagePolicyAdapter) export() interface{} { return exportImagePolicy(ex.ImagePolicy) } func (ex imagePolicyListAdapter) exportItem(i int) interface{} { return exportImagePolicy(&ex.ImagePolicyList.Items[i]) } ================================================ FILE: cmd/flux/export_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var exportImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Export ImageRepository resources in YAML format", Long: "The export image repository command exports one or all ImageRepository resources in YAML format.", Example: ` # Export all ImageRepository resources flux export image repository --all > image-repositories.yaml # Export a specific ImageRepository resource flux export image repository alpine > alpine.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: exportCommand{ object: imageRepositoryAdapter{&imagev1.ImageRepository{}}, list: imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, }.run, } func init() { exportImageCmd.AddCommand(exportImageRepositoryCmd) } func exportImageRepository(repo *imagev1.ImageRepository) interface{} { gvk := imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind) export := imagev1.ImageRepository{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: repo.Name, Namespace: repo.Namespace, Labels: repo.Labels, Annotations: repo.Annotations, }, Spec: repo.Spec, } return export } func (ex imageRepositoryAdapter) export() interface{} { return exportImageRepository(ex.ImageRepository) } func (ex imageRepositoryListAdapter) exportItem(i int) interface{} { return exportImageRepository(&ex.ImageRepositoryList.Items[i]) } ================================================ FILE: cmd/flux/export_image_update.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" autov1 "github.com/fluxcd/image-automation-controller/api/v1" ) var exportImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Export ImageUpdateAutomation resources in YAML format", Long: "The export image update command exports one or all ImageUpdateAutomation resources in YAML format.", Example: ` # Export all ImageUpdateAutomation resources flux export image update --all > updates.yaml # Export a specific automation flux export image update latest-images > latest.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: exportCommand{ object: imageUpdateAutomationAdapter{&autov1.ImageUpdateAutomation{}}, list: imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, }.run, } func init() { exportImageCmd.AddCommand(exportImageUpdateCmd) } // exportImageUpdate returns a value which has extraneous information // stripped out. func exportImageUpdate(item *autov1.ImageUpdateAutomation) interface{} { gvk := autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind) export := autov1.ImageUpdateAutomation{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: item.Name, Namespace: item.Namespace, Labels: item.Labels, Annotations: item.Annotations, }, Spec: item.Spec, } return export } func (ex imageUpdateAutomationAdapter) export() interface{} { return exportImageUpdate(ex.ImageUpdateAutomation) } func (ex imageUpdateAutomationListAdapter) exportItem(i int) interface{} { return exportImageUpdate(&ex.ImageUpdateAutomationList.Items[i]) } ================================================ FILE: cmd/flux/export_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var exportKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Export Kustomization resources in YAML format", Long: `The export kustomization command exports one or all Kustomization resources in YAML format.`, Example: ` # Export all Kustomization resources flux export kustomization --all > kustomizations.yaml # Export a Kustomization flux export kustomization my-app > kustomization.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: exportCommand{ object: kustomizationAdapter{&kustomizev1.Kustomization{}}, list: kustomizationListAdapter{&kustomizev1.KustomizationList{}}, }.run, } func init() { exportCmd.AddCommand(exportKsCmd) } func exportKs(kustomization *kustomizev1.Kustomization) interface{} { gvk := kustomizev1.GroupVersion.WithKind("Kustomization") export := kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: kustomization.Name, Namespace: kustomization.Namespace, Labels: kustomization.Labels, Annotations: kustomization.Annotations, }, Spec: kustomization.Spec, } return export } func (ex kustomizationAdapter) export() interface{} { return exportKs(ex.Kustomization) } func (ex kustomizationListAdapter) exportItem(i int) interface{} { return exportKs(&ex.KustomizationList.Items[i]) } ================================================ FILE: cmd/flux/export_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var exportReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Export Receiver resources in YAML format", Long: `The export receiver command exports one or all Receiver resources in YAML format.`, Example: ` # Export all Receiver resources flux export receiver --all > receivers.yaml # Export a Receiver flux export receiver main > main.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: exportCommand{ list: receiverListAdapter{¬ificationv1.ReceiverList{}}, object: receiverAdapter{¬ificationv1.Receiver{}}, }.run, } func init() { exportCmd.AddCommand(exportReceiverCmd) } func exportReceiver(receiver *notificationv1.Receiver) interface{} { gvk := notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind) export := notificationv1.Receiver{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: receiver.Name, Namespace: receiver.Namespace, Labels: receiver.Labels, Annotations: receiver.Annotations, }, Spec: receiver.Spec, } return export } func (ex receiverAdapter) export() interface{} { return exportReceiver(ex.Receiver) } func (ex receiverListAdapter) exportItem(i int) interface{} { return exportReceiver(&ex.ReceiverList.Items[i]) } ================================================ FILE: cmd/flux/export_secret.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/v2/internal/utils" ) // exportableWithSecret represents a type that you can fetch from the Kubernetes // API, get a secretRef from the spec, then tidy up for serialising. type exportableWithSecret interface { adapter exportable secret() *types.NamespacedName } // exportableWithSecretList represents a type that has a list of values, each of // which is exportableWithSecret. type exportableWithSecretList interface { listAdapter exportableList secretItem(i int) *types.NamespacedName } type exportWithSecretCommand struct { object exportableWithSecret list exportableWithSecretList } func (export exportWithSecretCommand) run(cmd *cobra.Command, args []string) error { if !exportArgs.all && len(args) < 1 { return fmt.Errorf("name is required") } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if exportArgs.all { err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(*kubeconfigArgs.Namespace)) if err != nil { return err } if export.list.len() == 0 { return fmt.Errorf("no objects found in %s namespace", *kubeconfigArgs.Namespace) } for i := 0; i < export.list.len(); i++ { if err = printExport(export.list.exportItem(i)); err != nil { return err } if exportSourceWithCred { if export.list.secretItem(i) != nil { namespacedName := *export.list.secretItem(i) return printSecretCredentials(ctx, kubeClient, namespacedName) } } } } else { name := args[0] namespacedName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, export.object.asClientObject()) if err != nil { return err } if err := printExport(export.object.export()); err != nil { return err } if exportSourceWithCred { if export.object.secret() != nil { namespacedName := *export.object.secret() return printSecretCredentials(ctx, kubeClient, namespacedName) } } } return nil } func printSecretCredentials(ctx context.Context, kubeClient client.Client, nsName types.NamespacedName) error { var cred corev1.Secret err := kubeClient.Get(ctx, nsName, &cred) if err != nil { return fmt.Errorf("failed to retrieve secret %s, error: %w", nsName.Name, err) } exported := corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: nsName.Name, Namespace: nsName.Namespace, }, Data: cred.Data, Type: cred.Type, } return printExport(exported) } ================================================ FILE: cmd/flux/export_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var exportSourceCmd = &cobra.Command{ Use: "source", Short: "Export sources", Long: `The export source sub-commands export sources in YAML format.`, } var ( exportSourceWithCred bool ) func init() { exportSourceCmd.PersistentFlags().BoolVar(&exportSourceWithCred, "with-credentials", false, "include credential secrets") exportCmd.AddCommand(exportSourceCmd) } ================================================ FILE: cmd/flux/export_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Export Bucket sources in YAML format", Long: "The export source git command exports one or all Bucket sources in YAML format.", Example: ` # Export all Bucket sources flux export source bucket --all > sources.yaml # Export a Bucket source including the static credentials flux export source bucket my-bucket --with-credentials > source.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: exportWithSecretCommand{ list: bucketListAdapter{&sourcev1.BucketList{}}, object: bucketAdapter{&sourcev1.Bucket{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceBucketCmd) } func exportBucket(source *sourcev1.Bucket) interface{} { gvk := sourcev1.GroupVersion.WithKind(sourcev1.BucketKind) export := sourcev1.Bucket{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func getBucketSecret(source *sourcev1.Bucket) *types.NamespacedName { if source.Spec.SecretRef != nil { namespacedName := types.NamespacedName{ Namespace: source.Namespace, Name: source.Spec.SecretRef.Name, } return &namespacedName } return nil } func (ex bucketAdapter) secret() *types.NamespacedName { return getBucketSecret(ex.Bucket) } func (ex bucketListAdapter) secretItem(i int) *types.NamespacedName { return getBucketSecret(&ex.BucketList.Items[i]) } func (ex bucketAdapter) export() interface{} { return exportBucket(ex.Bucket) } func (ex bucketListAdapter) exportItem(i int) interface{} { return exportBucket(&ex.BucketList.Items[i]) } ================================================ FILE: cmd/flux/export_source_chart.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Export HelmChart sources in YAML format", Long: withPreviewNote("The export source chart command exports one or all HelmChart sources in YAML format."), Example: ` # Export all chart sources flux export source chart --all > sources.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: exportCommand{ list: helmChartListAdapter{&sourcev1.HelmChartList{}}, object: helmChartAdapter{&sourcev1.HelmChart{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceChartCmd) } func exportHelmChart(source *sourcev1.HelmChart) interface{} { gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind) export := sourcev1.HelmChart{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func (ex helmChartAdapter) export() interface{} { return exportHelmChart(ex.HelmChart) } func (ex helmChartListAdapter) exportItem(i int) interface{} { return exportHelmChart(&ex.HelmChartList.Items[i]) } ================================================ FILE: cmd/flux/export_source_external.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceExternalCmd = &cobra.Command{ Use: "external [name]", Short: "Export ExternalArtifact sources in YAML format", Long: "The export source external command exports one or all ExternalArtifact sources in YAML format.", Example: ` # Export all ExternalArtifact sources flux export source external --all > sources.yaml # Export a specific ExternalArtifact flux export source external my-artifact > source.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)), RunE: exportWithSecretCommand{ list: externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}}, object: externalArtifactAdapter{&sourcev1.ExternalArtifact{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceExternalCmd) } func exportExternalArtifact(source *sourcev1.ExternalArtifact) any { gvk := sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind) export := sourcev1.ExternalArtifact{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func getExternalArtifactSecret(source *sourcev1.ExternalArtifact) *types.NamespacedName { // ExternalArtifact does not have a secretRef in its spec, this satisfies the interface return nil } func (ex externalArtifactAdapter) secret() *types.NamespacedName { return getExternalArtifactSecret(ex.ExternalArtifact) } func (ex externalArtifactListAdapter) secretItem(i int) *types.NamespacedName { return getExternalArtifactSecret(&ex.ExternalArtifactList.Items[i]) } func (ex externalArtifactAdapter) export() any { return exportExternalArtifact(ex.ExternalArtifact) } func (ex externalArtifactListAdapter) exportItem(i int) any { return exportExternalArtifact(&ex.ExternalArtifactList.Items[i]) } ================================================ FILE: cmd/flux/export_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Export GitRepository sources in YAML format", Long: `The export source git command exports one or all GitRepository sources in YAML format.`, Example: ` # Export all GitRepository sources flux export source git --all > sources.yaml # Export a GitRepository source including the SSH key pair or basic auth credentials flux export source git my-private-repo --with-credentials > source.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: exportWithSecretCommand{ object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, list: gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceGitCmd) } func exportGit(source *sourcev1.GitRepository) interface{} { gvk := sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind) export := sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func getGitSecret(source *sourcev1.GitRepository) *types.NamespacedName { if source.Spec.SecretRef != nil { namespacedName := types.NamespacedName{ Namespace: source.Namespace, Name: source.Spec.SecretRef.Name, } return &namespacedName } return nil } func (ex gitRepositoryAdapter) secret() *types.NamespacedName { return getGitSecret(ex.GitRepository) } func (ex gitRepositoryListAdapter) secretItem(i int) *types.NamespacedName { return getGitSecret(&ex.GitRepositoryList.Items[i]) } func (ex gitRepositoryAdapter) export() interface{} { return exportGit(ex.GitRepository) } func (ex gitRepositoryListAdapter) exportItem(i int) interface{} { return exportGit(&ex.GitRepositoryList.Items[i]) } ================================================ FILE: cmd/flux/export_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Export HelmRepository sources in YAML format", Long: "The export source git command exports one or all HelmRepository sources in YAML format.", Example: ` # Export all HelmRepository sources flux export source helm --all > sources.yaml # Export a HelmRepository source including the basic auth credentials flux export source helm my-private-repo --with-credentials > source.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: exportWithSecretCommand{ list: helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceHelmCmd) } func exportHelmRepository(source *sourcev1.HelmRepository) interface{} { gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind) export := sourcev1.HelmRepository{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func getHelmSecret(source *sourcev1.HelmRepository) *types.NamespacedName { if source.Spec.SecretRef != nil { namespacedName := types.NamespacedName{ Namespace: source.Namespace, Name: source.Spec.SecretRef.Name, } return &namespacedName } return nil } func (ex helmRepositoryAdapter) secret() *types.NamespacedName { return getHelmSecret(ex.HelmRepository) } func (ex helmRepositoryListAdapter) secretItem(i int) *types.NamespacedName { return getHelmSecret(&ex.HelmRepositoryList.Items[i]) } func (ex helmRepositoryAdapter) export() interface{} { return exportHelmRepository(ex.HelmRepository) } func (ex helmRepositoryListAdapter) exportItem(i int) interface{} { return exportHelmRepository(&ex.HelmRepositoryList.Items[i]) } ================================================ FILE: cmd/flux/export_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Export OCIRepository sources in YAML format", Long: withPreviewNote("The export source oci command exports one or all OCIRepository sources in YAML format."), Example: ` # Export all OCIRepository sources flux export source oci --all > sources.yaml # Export a OCIRepository including the static credentials flux export source oci my-app --with-credentials > source.yaml`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: exportWithSecretCommand{ list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, }.run, } func init() { exportSourceCmd.AddCommand(exportSourceOCIRepositoryCmd) } func exportOCIRepository(source *sourcev1.OCIRepository) interface{} { gvk := sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind) export := sourcev1.OCIRepository{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: source.Name, Namespace: source.Namespace, Labels: source.Labels, Annotations: source.Annotations, }, Spec: source.Spec, } return export } func getOCIRepositorySecret(source *sourcev1.OCIRepository) *types.NamespacedName { if source.Spec.SecretRef != nil { namespacedName := types.NamespacedName{ Namespace: source.Namespace, Name: source.Spec.SecretRef.Name, } return &namespacedName } return nil } func (ex ociRepositoryAdapter) secret() *types.NamespacedName { return getOCIRepositorySecret(ex.OCIRepository) } func (ex ociRepositoryListAdapter) secretItem(i int) *types.NamespacedName { return getOCIRepositorySecret(&ex.OCIRepositoryList.Items[i]) } func (ex ociRepositoryAdapter) export() interface{} { return exportOCIRepository(ex.OCIRepository) } func (ex ociRepositoryListAdapter) exportItem(i int) interface{} { return exportOCIRepository(&ex.OCIRepositoryList.Items[i]) } ================================================ FILE: cmd/flux/export_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestExport(t *testing.T) { namespace := allocateNamespace("flux-system") objectFile := "testdata/export/objects.yaml" tmpl := map[string]string{ "fluxns": namespace, } testEnv.CreateObjectFile(objectFile, tmpl, t) cases := []struct { name string arg string goldenFile string tmpl map[string]string }{ { "alert-provider", "export alert-provider slack", "testdata/export/provider.yaml", tmpl, }, { "alert", "export alert flux-system", "testdata/export/alert.yaml", tmpl, }, { "image policy", "export image policy flux-system", "testdata/export/image-policy.yaml", tmpl, }, { "image repository", "export image repository flux-system", "testdata/export/image-repo.yaml", tmpl, }, { "image update", "export image update flux-system", "testdata/export/image-update.yaml", tmpl, }, { "source git", "export source git flux-system", "testdata/export/git-repo.yaml", tmpl, }, { "source chart", "export source chart flux-system", "testdata/export/helm-chart.yaml", tmpl, }, { "source helm", "export source helm flux-system", "testdata/export/helm-repo.yaml", tmpl, }, { "receiver", "export receiver flux-system", "testdata/export/receiver.yaml", tmpl, }, { "kustomization", "export kustomization flux-system", "testdata/export/ks.yaml", tmpl, }, { "helmrelease", "export helmrelease flux-system", "testdata/export/helm-release.yaml", tmpl, }, { "bucket", "export source bucket flux-system", "testdata/export/bucket.yaml", tmpl, }, { "source external", "export source external flux-system", "testdata/export/external-artifact.yaml", tmpl, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.arg + " -n=" + namespace, assert: assertGoldenTemplateFile(tt.goldenFile, tmpl), } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/get.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "strings" "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" watchtools "k8s.io/client-go/tools/watch" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/printers" ) type deriveType func(runtime.Object) (summarisable, error) type typeMap map[string]deriveType func (m typeMap) registerCommand(t string, f deriveType) error { if _, ok := m[t]; ok { return fmt.Errorf("duplicate type function %s", t) } m[t] = f return nil } func (m typeMap) execute(t string, obj runtime.Object) (summarisable, error) { f, ok := m[t] if !ok { return nil, fmt.Errorf("unsupported type %s", t) } return f(obj) } var getCmd = &cobra.Command{ Use: "get", Short: "Get the resources and their status", Long: `The get sub-commands print the statuses of Flux resources.`, } type GetFlags struct { allNamespaces bool noHeader bool statusSelector string labelSelector string watch bool } var getArgs GetFlags func init() { getCmd.PersistentFlags().BoolVarP(&getArgs.allNamespaces, "all-namespaces", "A", false, "list the requested object(s) across all namespaces") getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results") getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.") getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "", "specify the status condition name and the desired state to filter the get result, e.g. ready=false") getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "", "filter objects by label selector") rootCmd.AddCommand(getCmd) } type summarisable interface { listAdapter summariseItem(i int, includeNamespace bool, includeKind bool) []string headers(includeNamespace bool) []string statusSelectorMatches(i int, conditionType, conditionStatus string) bool } // --- these help with implementations of summarisable func statusAndMessage(conditions []metav1.Condition) (string, string) { if c := apimeta.FindStatusCondition(conditions, meta.ReadyCondition); c != nil { return string(c.Status), c.Message } return string(metav1.ConditionFalse), "waiting to be reconciled" } func statusMatches(conditionType, conditionStatus string, conditions []metav1.Condition) bool { // we don't use apimeta.FindStatusCondition because we'd like to use EqualFold to compare two strings var c *metav1.Condition for i := range conditions { if strings.EqualFold(conditions[i].Type, conditionType) { c = &conditions[i] } } if c != nil { return strings.EqualFold(string(c.Status), conditionStatus) } return false } func nameColumns(item named, includeNamespace bool, includeKind bool) []string { name := item.GetName() if includeKind { name = fmt.Sprintf("%s/%s", strings.ToLower(item.GetObjectKind().GroupVersionKind().Kind), item.GetName()) } if includeNamespace { return []string{item.GetNamespace(), name} } return []string{name} } var namespaceHeader = []string{"Namespace"} type getCommand struct { apiType list summarisable funcMap typeMap } func (get getCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var listOpts []client.ListOption if !getArgs.allNamespaces { listOpts = append(listOpts, client.InNamespace(*kubeconfigArgs.Namespace)) } if len(args) > 0 { listOpts = append(listOpts, client.MatchingFields{"metadata.name": args[0]}) } if getArgs.labelSelector != "" { label, err := metav1.ParseToLabelSelector(getArgs.labelSelector) if err != nil { return fmt.Errorf("unable to parse label selector: %w", err) } sel, err := metav1.LabelSelectorAsSelector(label) if err != nil { return err } listOpts = append(listOpts, client.MatchingLabelsSelector{ Selector: sel, }) } getAll := cmd.Use == "all" if getArgs.watch { return get.watch(ctx, kubeClient, cmd, args, listOpts) } err = kubeClient.List(ctx, get.list.asClientList(), listOpts...) if err != nil { if getAll && apimeta.IsNoMatchError(err) { return nil } return err } if get.list.len() == 0 { if len(args) > 0 { return fmt.Errorf("%s object '%s' not found in %s namespace", get.kind, args[0], namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace), ) } else if !getAll { return fmt.Errorf("no %s objects found in %s namespace", get.kind, namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace), ) } return nil } var header []string if !getArgs.noHeader { header = get.list.headers(getArgs.allNamespaces) } rows, err := getRowsToPrint(getAll, get.list) if err != nil { return err } err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows) if err != nil { return err } if getAll { fmt.Println() } return nil } func namespaceNameOrAny(allNamespaces bool, namespaceName string) string { if allNamespaces { return "any" } return fmt.Sprintf("%q", namespaceName) } func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) { noFilter := true var conditionType, conditionStatus string if getArgs.statusSelector != "" { parts := strings.SplitN(getArgs.statusSelector, "=", 2) if len(parts) != 2 { return nil, fmt.Errorf("expected status selector in type=status format, but found: %s", getArgs.statusSelector) } conditionType = parts[0] conditionStatus = parts[1] noFilter = false } var rows [][]string for i := 0; i < list.len(); i++ { if noFilter || list.statusSelectorMatches(i, conditionType, conditionStatus) { row := list.summariseItem(i, getArgs.allNamespaces, getAll) rows = append(rows, row) } } return rows, nil } // watch starts a client-side watch of one or more resources. func (get *getCommand) watch(ctx context.Context, kubeClient client.WithWatch, cmd *cobra.Command, args []string, listOpts []client.ListOption) error { w, err := kubeClient.Watch(ctx, get.list.asClientList(), listOpts...) if err != nil { return err } _, err = watchUntil(ctx, w, get) if err != nil { return err } return nil } func watchUntil(ctx context.Context, w watch.Interface, get *getCommand) (bool, error) { firstIteration := true _, error := watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) { objToPrint := e.Object sink, err := get.funcMap.execute(get.apiType.kind, objToPrint) if err != nil { return false, err } var header []string if !getArgs.noHeader { header = sink.headers(getArgs.allNamespaces) } rows, err := getRowsToPrint(false, sink) if err != nil { return false, err } if firstIteration { err = printers.TablePrinter(header).Print(os.Stdout, rows) if err != nil { return false, err } firstIteration = false } else { err = printers.TablePrinter([]string{}).Print(os.Stdout, rows) if err != nil { return false, err } } return false, nil }) return false, error } func validateWatchOption(cmd *cobra.Command, toMatch string) error { w, _ := cmd.Flags().GetBool("watch") if cmd.Use == toMatch && w { return fmt.Errorf("expected a single resource type, but found %s", cmd.Use) } return nil } ================================================ FILE: cmd/flux/get_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAlertCmd = &cobra.Command{ Use: "alerts", Aliases: []string{"alert"}, Short: "Get Alert statuses", Long: withPreviewNote("The get alert command prints the statuses of the resources."), Example: ` # List all Alerts and their status flux get alerts`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: alertType, list: &alertListAdapter{¬ificationv1.AlertList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*notificationv1.Alert) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v alert", obj) } sink := alertListAdapter{ ¬ificationv1.AlertList{ Items: []notificationv1.Alert{ *o, }, }, } return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getCmd.AddCommand(getAlertCmd) } func (s alertListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := string(metav1.ConditionTrue), "Alert is Ready" return append(nameColumns(&item, includeNamespace, includeKind), cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s alertListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { return false } ================================================ FILE: cmd/flux/get_alertprovider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAlertProviderCmd = &cobra.Command{ Use: "alert-providers", Aliases: []string{"alert-provider"}, Short: "Get Provider statuses", Long: withPreviewNote("The get alert-provider command prints the statuses of the resources."), Example: ` # List all Providers and their status flux get alert-providers`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: alertProviderType, list: alertProviderListAdapter{¬ificationv1.ProviderList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*notificationv1.Provider) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v alert-provider", obj) } sink := alertProviderListAdapter{ ¬ificationv1.ProviderList{ Items: []notificationv1.Provider{ *o, }, }, } return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getCmd.AddCommand(getAlertProviderCmd) } func (s alertProviderListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := string(metav1.ConditionTrue), "Provider is Ready" return append(nameColumns(&item, includeNamespace, includeKind), status, msg) } func (s alertProviderListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { return false } ================================================ FILE: cmd/flux/get_all.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" helmv2 "github.com/fluxcd/helm-controller/api/v2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAllCmd = &cobra.Command{ Use: "all", Short: "Get all resources and statuses", Long: withPreviewNote("The get all command print the statuses of all resources."), Example: ` # List all resources in a namespace flux get all --namespace=flux-system # List all resources in all namespaces flux get all --all-namespaces`, RunE: func(cmd *cobra.Command, args []string) error { err := validateWatchOption(cmd, "all") if err != nil { return err } err = getSourceAllCmd.RunE(cmd, args) if err != nil { logError(err) } // all get command var allCmd = []getCommand{ { apiType: helmReleaseType, list: &helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, }, { apiType: kustomizationType, list: &kustomizationListAdapter{&kustomizev1.KustomizationList{}}, }, { apiType: receiverType, list: receiverListAdapter{¬ificationv1.ReceiverList{}}, }, { apiType: alertProviderType, list: alertProviderListAdapter{¬ificationv1b3.ProviderList{}}, }, { apiType: alertType, list: &alertListAdapter{¬ificationv1b3.AlertList{}}, }, } err = getImageAllCmd.RunE(cmd, args) if err != nil { logError(err) } for _, c := range allCmd { if err := c.run(cmd, args); err != nil { logError(err) } } return nil }, } func logError(err error) { if !apimeta.IsNoMatchError(err) { logger.Failuref("%s", err.Error()) } } func init() { getCmd.AddCommand(getAllCmd) } ================================================ FILE: cmd/flux/get_artifact.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var getArtifactCmd = &cobra.Command{ Use: "artifacts", Aliases: []string{"artifact"}, Short: "Get artifact object status", Long: `The get artifact sub-commands print the status of artifact objects.`, } func init() { getCmd.AddCommand(getArtifactCmd) } ================================================ FILE: cmd/flux/get_artifact_generator.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" ) var getArtifactGeneratorCmd = &cobra.Command{ Use: "generators", Aliases: []string{"generator"}, Short: "Get artifact generator statuses", Long: `The get artifact generator command prints the statuses of the resources.`, Example: ` # List all ArtifactGenerators and their status flux get artifact generators`, ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: receiverType, list: artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*swapi.ArtifactGenerator) if !ok { return nil, fmt.Errorf("impossible to cast type %#v generator", obj) } sink := artifactGeneratorListAdapter{&swapi.ArtifactGeneratorList{ Items: []swapi.ArtifactGenerator{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getArtifactCmd.AddCommand(getArtifactGeneratorCmd) } func (s artifactGeneratorListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) return append(nameColumns(&item, includeNamespace, includeKind), cases.Title(language.English).String(strconv.FormatBool(item.IsDisabled())), status, msg) } func (s artifactGeneratorListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s artifactGeneratorListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := s.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var getHelmReleaseCmd = &cobra.Command{ Use: "helmreleases", Aliases: []string{"hr", "helmrelease"}, Short: "Get HelmRelease statuses", Long: "The get helmreleases command prints the statuses of the resources.", Example: ` # List all Helm releases and their status flux get helmreleases`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: helmReleaseType, list: &helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*helmv2.HelmRelease) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v helmrelease", obj) } sink := helmReleaseListAdapter{&helmv2.HelmReleaseList{ Items: []helmv2.HelmRelease{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getCmd.AddCommand(getHelmReleaseCmd) } func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string { if helmRelease.Status.History != nil && len(helmRelease.Status.History) > 0 { return helmRelease.Status.History[0].ChartVersion } return helmRelease.Status.LastAttemptedRevision } func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] revision := getHelmReleaseRevision(item) status, msg := statusAndMessage(item.Status.Conditions) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmReleaseListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a helmReleaseListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var getImageCmd = &cobra.Command{ Use: "images", Aliases: []string{"image"}, Short: "Get image automation object status", Long: `The get image sub-commands print the status of image automation objects.`, } func init() { getCmd.AddCommand(getImageCmd) } ================================================ FILE: cmd/flux/get_image_all.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" autov1 "github.com/fluxcd/image-automation-controller/api/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var getImageAllCmd = &cobra.Command{ Use: "all", Short: "Get all image statuses", Long: "The get image sub-commands print the statuses of all image objects.", Example: ` # List all image objects in a namespace flux get images all --namespace=flux-system # List all image objects in all namespaces flux get images all --all-namespaces`, RunE: func(cmd *cobra.Command, args []string) error { err := validateWatchOption(cmd, "all") if err != nil { return err } var allImageCmd = []getCommand{ { apiType: imageRepositoryType, list: imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, }, { apiType: imagePolicyType, list: &imagePolicyListAdapter{&imagev1.ImagePolicyList{}}, }, { apiType: imageUpdateAutomationType, list: &imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, }, } for _, c := range allImageCmd { if err := c.run(cmd, args); err != nil { logger.Failuref("%s", err.Error()) } } return nil }, } func init() { getImageCmd.AddCommand(getImageAllCmd) } ================================================ FILE: cmd/flux/get_image_policy.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var getImagePolicyCmd = &cobra.Command{ Use: "policy", Short: "Get ImagePolicy status", Long: "The get image policy command prints the status of ImagePolicy objects.", Example: ` # List all image policies and their status flux get image policy # List image policies from all namespaces flux get image policy --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: imagePolicyType, list: &imagePolicyListAdapter{&imagev1.ImagePolicyList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*imagev1.ImagePolicy) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v policy", obj) } sink := imagePolicyListAdapter{&imagev1.ImagePolicyList{ Items: []imagev1.ImagePolicy{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getImageCmd.AddCommand(getImagePolicyCmd) } func (s imagePolicyListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) var image, tag string if ref := item.Status.LatestRef; ref != nil { image = ref.Name tag = ref.Tag } return append(nameColumns(&item, includeNamespace, includeKind), image, tag, status, msg) } func (s imagePolicyListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Image", "Tag", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s imagePolicyListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := s.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "time" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var getImageRepositoryCmd = &cobra.Command{ Use: "repository", Short: "Get ImageRepository status", Long: "The get image repository command prints the status of ImageRepository objects.", Example: ` # List all image repositories and their status flux get image repository # List image repositories from all namespaces flux get image repository --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: imageRepositoryType, list: imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*imagev1.ImageRepository) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v repository", obj) } sink := imageRepositoryListAdapter{&imagev1.ImageRepositoryList{ Items: []imagev1.ImageRepository{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getImageCmd.AddCommand(getImageRepositoryCmd) } func (s imageRepositoryListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) var lastScan string if item.Status.LastScanResult != nil { lastScan = item.Status.LastScanResult.ScanTime.Time.Format(time.RFC3339) } return append(nameColumns(&item, includeNamespace, includeKind), lastScan, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s imageRepositoryListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Last scan", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s imageRepositoryListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := s.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_image_update.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "time" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" autov1 "github.com/fluxcd/image-automation-controller/api/v1" ) var getImageUpdateCmd = &cobra.Command{ Use: "update", Short: "Get ImageUpdateAutomation status", Long: "The get image update command prints the status of ImageUpdateAutomation objects.", Example: ` # List all image update automation object and their status flux get image update # List image update automations from all namespaces flux get image update --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: imageUpdateAutomationType, list: &imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*autov1.ImageUpdateAutomation) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v update", obj) } sink := imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{ Items: []autov1.ImageUpdateAutomation{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getImageCmd.AddCommand(getImageUpdateCmd) } func (s imageUpdateAutomationListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) var lastRun string if item.Status.LastAutomationRunTime != nil { lastRun = item.Status.LastAutomationRunTime.Time.Format(time.RFC3339) } return append(nameColumns(&item, includeNamespace, includeKind), lastRun, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s imageUpdateAutomationListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Last run", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s imageUpdateAutomationListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := s.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getKsCmd = &cobra.Command{ Use: "kustomizations", Aliases: []string{"ks", "kustomization"}, Short: "Get Kustomization statuses", Long: `The get kustomizations command prints the statuses of the resources.`, Example: ` # List all kustomizations and their status flux get kustomizations`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: kustomizationType, list: &kustomizationListAdapter{&kustomizev1.KustomizationList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*kustomizev1.Kustomization) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v kustomization", obj) } sink := kustomizationListAdapter{ &kustomizev1.KustomizationList{ Items: []kustomizev1.Kustomization{ *o, }, }, } return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getCmd.AddCommand(getKsCmd) } func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] revision := item.Status.LastAppliedRevision status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a kustomizationListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a kustomizationListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var getReceiverCmd = &cobra.Command{ Use: "receivers", Aliases: []string{"receiver"}, Short: "Get Receiver statuses", Long: `The get receiver command prints the statuses of the resources.`, Example: ` # List all Receiver and their status flux get receivers`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: receiverType, list: receiverListAdapter{¬ificationv1.ReceiverList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*notificationv1.Receiver) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v receiver", obj) } sink := receiverListAdapter{¬ificationv1.ReceiverList{ Items: []notificationv1.Receiver{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getCmd.AddCommand(getReceiverCmd) } func (s receiverListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) return append(nameColumns(&item, includeNamespace, includeKind), cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s receiverListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } return headers } func (s receiverListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := s.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var getSourceCmd = &cobra.Command{ Use: "sources", Aliases: []string{"source"}, Short: "Get source statuses", Long: `The get source sub-commands print the statuses of the sources.`, } func init() { getCmd.AddCommand(getSourceCmd) } ================================================ FILE: cmd/flux/get_source_all.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var getSourceAllCmd = &cobra.Command{ Use: "all", Short: "Get all source statuses", Long: withPreviewNote("The get sources all command print the statuses of all sources."), Example: ` # List all sources in a namespace flux get sources all --namespace=flux-system # List all sources in all namespaces flux get sources all --all-namespaces`, RunE: func(cmd *cobra.Command, args []string) error { err := validateWatchOption(cmd, "all") if err != nil { return err } var allSourceCmd = []getCommand{ { apiType: ociRepositoryType, list: &ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, }, { apiType: bucketType, list: &bucketListAdapter{&sourcev1.BucketList{}}, }, { apiType: gitRepositoryType, list: &gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, }, { apiType: helmRepositoryType, list: &helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, }, { apiType: helmChartType, list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, }, { apiType: externalArtifactType, list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}}, }, } for _, c := range allSourceCmd { if err := c.run(cmd, args); err != nil { if !apimeta.IsNoMatchError(err) { logger.Failuref("%s", err.Error()) } } } return nil }, } func init() { getSourceCmd.AddCommand(getSourceAllCmd) } ================================================ FILE: cmd/flux/get_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceBucketCmd = &cobra.Command{ Use: "bucket", Short: "Get Bucket source statuses", Long: "The get sources bucket command prints the status of the Bucket sources.", Example: ` # List all Buckets and their status flux get sources bucket # List buckets from all namespaces flux get sources helm --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: bucketType, list: &bucketListAdapter{&sourcev1.BucketList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.Bucket) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v bucket", obj) } sink := &bucketListAdapter{&sourcev1.BucketList{ Items: []sourcev1.Bucket{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceBucketCmd) } func (a *bucketListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a bucketListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a bucketListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source_chart.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceHelmChartCmd = &cobra.Command{ Use: "chart", Short: "Get HelmChart statuses", Long: "The get sources chart command prints the status of the HelmCharts.", Example: ` # List all Helm charts and their status flux get sources chart # List Helm charts from all namespaces flux get sources chart --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: helmChartType, list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.HelmChart) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v chart", obj) } sink := &helmChartListAdapter{&sourcev1.HelmChartList{ Items: []sourcev1.HelmChart{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceHelmChartCmd) } func (a *helmChartListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) // NB: do not shorten revision as it contains a SemVer // Message may still contain reference of e.g. commit chart was build from msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmChartListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a helmChartListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source_external.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceExternalCmd = &cobra.Command{ Use: "external", Short: "Get ExternalArtifact source statuses", Long: `The get sources external command prints the status of the ExternalArtifact sources.`, Example: ` # List all ExternalArtifacts and their status flux get sources external # List ExternalArtifacts from all namespaces flux get sources external --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.ExternalArtifactKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: externalArtifactType, list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.ExternalArtifact) if !ok { return nil, fmt.Errorf("impossible to cast type %#v to ExternalArtifact", obj) } sink := &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{ Items: []sourcev1.ExternalArtifact{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceExternalCmd) } func (a *externalArtifactListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.Status.Artifact != nil { revision = item.Status.Artifact.Revision } status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) var source string if item.Spec.SourceRef != nil { source = fmt.Sprintf("%s/%s/%s", item.Spec.SourceRef.Kind, item.Spec.SourceRef.Namespace, item.Spec.SourceRef.Name) } return append(nameColumns(&item, includeNamespace, includeKind), revision, source, status, msg) } func (a externalArtifactListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Source", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a externalArtifactListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceGitCmd = &cobra.Command{ Use: "git", Short: "Get GitRepository source statuses", Long: `The get sources git command prints the status of the GitRepository sources.`, Example: ` # List all Git repositories and their status flux get sources git # List Git repositories from all namespaces flux get sources git --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: gitRepositoryType, list: &gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.GitRepository) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v git", obj) } sink := &gitRepositoryListAdapter{&sourcev1.GitRepositoryList{ Items: []sourcev1.GitRepository{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceGitCmd) } func (a *gitRepositoryListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a gitRepositoryListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a gitRepositoryListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceHelmCmd = &cobra.Command{ Use: "helm", Short: "Get HelmRepository source statuses", Long: "The get sources helm command prints the status of the HelmRepository sources.", Example: ` # List all Helm repositories and their status flux get sources helm # List Helm repositories from all namespaces flux get sources helm --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: helmRepositoryType, list: &helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.HelmRepository) if !ok { return nil, fmt.Errorf("Impossible to cast type %#v helm", obj) } sink := &helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{ Items: []sourcev1.HelmRepository{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceHelmCmd) } func (a *helmRepositoryListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } var status, msg string if item.Spec.Type == sourcev1.HelmRepositoryTypeOCI { status, msg = string(metav1.ConditionTrue), "Helm repository is Ready" } else { status, msg = statusAndMessage(item.Status.Conditions) } revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmRepositoryListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a helmRepositoryListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strconv" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci", Short: "Get OCIRepository status", Long: withPreviewNote("The get sources oci command prints the status of the OCIRepository sources."), Example: ` # List all OCIRepositories and their status flux get sources oci # List OCIRepositories from all namespaces flux get sources oci --all-namespaces`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ apiType: ociRepositoryType, list: &ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, funcMap: make(typeMap), } err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { o, ok := obj.(*sourcev1.OCIRepository) if !ok { return nil, fmt.Errorf("impossible to cast type %#v to OCIRepository", obj) } sink := &ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{ Items: []sourcev1.OCIRepository{ *o, }}} return sink, nil }) if err != nil { return err } if err := get.run(cmd, args); err != nil { return err } return nil }, } func init() { getSourceCmd.AddCommand(getSourceOCIRepositoryCmd) } func (a *ociRepositoryListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] var revision string if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a ociRepositoryListAdapter) headers(includeNamespace bool) []string { headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } return headers } func (a ociRepositoryListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } ================================================ FILE: cmd/flux/get_test.go ================================================ //go:build unit // +build unit /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "testing" ) func Test_GetCmd(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } testEnv.CreateObjectFile("./testdata/get/objects.yaml", tmpl, t) tests := []struct { name string args string expected string }{ { name: "no label selector", expected: "testdata/get/get.golden", }, { name: "equal label selector", args: "-l sharding.fluxcd.io/key=shard1", expected: "testdata/get/get_label_one.golden", }, { name: "notin label selector", args: `-l "sharding.fluxcd.io/key notin (shard1, shard2)"`, expected: "testdata/get/get_label_two.golden", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: "get sources git " + tt.args + " -n " + tmpl["fluxns"], assert: assertGoldenTemplateFile(tt.expected, nil), } cmd.runTestCmd(t) }) } } func Test_GetCmdErrors(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } testEnv.CreateObjectFile("./testdata/get/objects.yaml", tmpl, t) tests := []struct { name string args string assert assertFunc }{ { name: "specific object not found", args: "get kustomization non-existent-resource -n " + tmpl["fluxns"], assert: assertError(fmt.Sprintf("Kustomization object 'non-existent-resource' not found in \"%s\" namespace", tmpl["fluxns"])), }, { name: "no objects found in namespace", args: "get helmrelease -n " + tmpl["fluxns"], assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } func Test_GetCmdSuccess(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } testEnv.CreateObjectFile("./testdata/get/objects.yaml", tmpl, t) tests := []struct { name string args string assert assertFunc }{ { name: "list sources git", args: "get sources git -n " + tmpl["fluxns"], assert: assertSuccess(), }, { name: "get help", args: "get --help", assert: assertSuccess(), }, { name: "get with all namespaces flag", args: "get sources git -A", assert: assertSuccess(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) // helmv2.HelmRelease var helmReleaseType = apiType{ kind: helmv2.HelmReleaseKind, humanKind: "helmrelease", groupVersion: helmv2.GroupVersion, } type helmReleaseAdapter struct { *helmv2.HelmRelease } func (h helmReleaseAdapter) asClientObject() client.Object { return h.HelmRelease } func (h helmReleaseAdapter) deepCopyClientObject() client.Object { return h.HelmRelease.DeepCopy() } // helmv2.HelmReleaseList type helmReleaseListAdapter struct { *helmv2.HelmReleaseList } func (h helmReleaseListAdapter) asClientList() client.ObjectList { return h.HelmReleaseList } func (h helmReleaseListAdapter) len() int { return len(h.HelmReleaseList.Items) } ================================================ FILE: cmd/flux/helmrelease_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "testing" func TestHelmReleaseFromGit(t *testing.T) { namespace := allocateNamespace("thrfg") del, err := execSetupTestNamespace(namespace) if err != nil { t.Fatal(err) } t.Cleanup(del) tmpl := map[string]string{"ns": namespace} cases := []struct { args string goldenFile string tmpl map[string]string }{ { "create source git thrfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.3.5", "testdata/helmrelease/create_source_git.golden", nil, }, { "create helmrelease thrfg --source=GitRepository/thrfg --chart=./charts/podinfo", "testdata/helmrelease/create_helmrelease_from_git.golden", nil, }, { "get helmrelease thrfg", "testdata/helmrelease/get_helmrelease_from_git.golden", nil, }, { "reconcile helmrelease thrfg --with-source", "testdata/helmrelease/reconcile_helmrelease_from_git.golden", tmpl, }, { "suspend helmrelease thrfg", "testdata/helmrelease/suspend_helmrelease_from_git.golden", tmpl, }, { "resume helmrelease thrfg", "testdata/helmrelease/resume_helmrelease_from_git.golden", tmpl, }, { "delete helmrelease thrfg --silent", "testdata/helmrelease/delete_helmrelease_from_git.golden", tmpl, }, } for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) } } ================================================ FILE: cmd/flux/image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "sigs.k8s.io/controller-runtime/pkg/client" autov1 "github.com/fluxcd/image-automation-controller/api/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) // These are general-purpose adapters for attaching methods to, for // the various commands. The *List adapters implement len(), since // it's used in at least a couple of commands. // imagev1.ImageRepository var imageRepositoryType = apiType{ kind: imagev1.ImageRepositoryKind, humanKind: "image repository", groupVersion: imagev1.GroupVersion, } type imageRepositoryAdapter struct { *imagev1.ImageRepository } func (a imageRepositoryAdapter) asClientObject() client.Object { return a.ImageRepository } func (a imageRepositoryAdapter) deepCopyClientObject() client.Object { return a.ImageRepository.DeepCopy() } // imagev1.ImageRepositoryList type imageRepositoryListAdapter struct { *imagev1.ImageRepositoryList } func (a imageRepositoryListAdapter) asClientList() client.ObjectList { return a.ImageRepositoryList } func (a imageRepositoryListAdapter) len() int { return len(a.ImageRepositoryList.Items) } // imagev1.ImagePolicy var imagePolicyType = apiType{ kind: imagev1.ImagePolicyKind, humanKind: "image policy", groupVersion: imagev1.GroupVersion, } type imagePolicyAdapter struct { *imagev1.ImagePolicy } func (a imagePolicyAdapter) asClientObject() client.Object { return a.ImagePolicy } func (a imagePolicyAdapter) deepCopyClientObject() client.Object { return a.ImagePolicy.DeepCopy() } func (a imagePolicyAdapter) isStatic() bool { return false } func (a imagePolicyAdapter) lastHandledReconcileRequest() string { return a.Status.GetLastHandledReconcileRequest() } func (a imagePolicyAdapter) isSuspended() bool { return a.Spec.Suspend } func (a imagePolicyAdapter) setSuspended() { a.Spec.Suspend = true } func (a imagePolicyAdapter) successMessage() string { return fmt.Sprintf("selected ref %s", a.Status.LatestRef.String()) } func (a imagePolicyAdapter) setUnsuspended() { a.Spec.Suspend = false } // imagev1.ImagePolicyList type imagePolicyListAdapter struct { *imagev1.ImagePolicyList } func (a imagePolicyListAdapter) asClientList() client.ObjectList { return a.ImagePolicyList } func (a imagePolicyListAdapter) len() int { return len(a.ImagePolicyList.Items) } func (a imagePolicyListAdapter) resumeItem(i int) resumable { return &imagePolicyAdapter{&a.ImagePolicyList.Items[i]} } func (obj imagePolicyAdapter) getObservedGeneration() int64 { return obj.ImagePolicy.Status.ObservedGeneration } func (a imagePolicyListAdapter) item(i int) suspendable { return &imagePolicyAdapter{&a.ImagePolicyList.Items[i]} } // autov1.ImageUpdateAutomation var imageUpdateAutomationType = apiType{ kind: autov1.ImageUpdateAutomationKind, humanKind: "image update automation", groupVersion: autov1.GroupVersion, } type imageUpdateAutomationAdapter struct { *autov1.ImageUpdateAutomation } func (a imageUpdateAutomationAdapter) asClientObject() client.Object { return a.ImageUpdateAutomation } func (a imageUpdateAutomationAdapter) deepCopyClientObject() client.Object { return a.ImageUpdateAutomation.DeepCopy() } // autov1.ImageUpdateAutomationList type imageUpdateAutomationListAdapter struct { *autov1.ImageUpdateAutomationList } func (a imageUpdateAutomationListAdapter) asClientList() client.ObjectList { return a.ImageUpdateAutomationList } func (a imageUpdateAutomationListAdapter) len() int { return len(a.ImageUpdateAutomationList.Items) } ================================================ FILE: cmd/flux/image_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "testing" func TestImageScanning(t *testing.T) { namespace := allocateNamespace("tis") del, err := execSetupTestNamespace(namespace) if err != nil { t.Fatal(err) } t.Cleanup(del) cases := []struct { args string goldenFile string }{ { "create image repository podinfo --image=ghcr.io/stefanprodan/podinfo --interval=10m", "testdata/image/create_image_repository.golden", }, { "create image policy podinfo-semver --image-ref=podinfo --interval=10m --reflect-digest=Always --select-semver=5.0.x", "testdata/image/create_image_policy.golden", }, { "get image policy podinfo-semver", "testdata/image/get_image_policy_semver.golden", }, { `create image policy podinfo-regex --image-ref=podinfo --select-semver=">4.0.0" --filter-regex="5\.0\.0"`, "testdata/image/create_image_policy.golden", }, { "get image policy podinfo-regex", "testdata/image/get_image_policy_regex.golden", }, { "suspend image policy podinfo-semver", "testdata/image/suspend_image_policy.golden", }, { "resume image policy podinfo-semver", "testdata/image/resume_image_policy.golden", }, { "reconcile image policy podinfo-semver", "testdata/image/reconcile_image_policy.golden", }, } for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, assert: assertGoldenFile(tc.goldenFile), } cmd.runTestCmd(t) } } ================================================ FILE: cmd/flux/install.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/manifoldco/promptui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/status" ) var installCmd = &cobra.Command{ Use: "install", Args: cobra.NoArgs, Short: "Install or upgrade Flux", Long: `The install command deploys Flux in the specified namespace. If a previous version is installed, then an in-place upgrade will be performed.`, Example: ` # Install the latest version in the flux-system namespace flux install --namespace=flux-system # Install a specific series of components flux install --components="source-controller,kustomize-controller" # Install all components including the image automation ones flux install --components-extra="image-reflector-controller,image-automation-controller" # Install Flux onto tainted Kubernetes nodes flux install --toleration-keys=node.kubernetes.io/dedicated-to-flux # Dry-run install flux install --export | kubectl apply --dry-run=client -f- # Write install manifests to file flux install --export > flux-system.yaml`, RunE: installCmdRun, } type installFlags struct { export bool version string defaultComponents []string extraComponents []string registry string registryCredential string imagePullSecret string branch string watchAllNamespaces bool networkPolicy bool manifestsPath string logLevel flags.LogLevel tokenAuth bool clusterDomain string tolerationKeys []string force bool } var installArgs = newInstallFlags() func init() { installCmd.Flags().BoolVar(&installArgs.export, "export", false, "write the install manifests to stdout and exit") installCmd.Flags().StringVarP(&installArgs.version, "version", "v", "", "toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases") installCmd.Flags().StringSliceVar(&installArgs.defaultComponents, "components", rootArgs.defaults.Components, "list of components, accepts comma-separated values") installCmd.Flags().StringSliceVar(&installArgs.extraComponents, "components-extra", nil, "list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller,source-watcher'") installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory") installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry, "container registry where the toolkit images are published") installCmd.Flags().StringVar(&installArgs.registryCredential, "registry-creds", "", "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") installCmd.Flags().StringVar(&installArgs.imagePullSecret, "image-pull-secret", "", "Kubernetes secret name used for pulling the toolkit images from a private registry") installCmd.Flags().BoolVar(&installArgs.watchAllNamespaces, "watch-all-namespaces", rootArgs.defaults.WatchAllNamespaces, "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed") installCmd.Flags().Var(&installArgs.logLevel, "log-level", installArgs.logLevel.Description()) installCmd.Flags().BoolVar(&installArgs.networkPolicy, "network-policy", rootArgs.defaults.NetworkPolicy, "deny ingress access to the toolkit controllers from other namespaces using network policies") installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil, "list of toleration keys used to schedule the components pods onto nodes with matching taints") installCmd.Flags().BoolVar(&installArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm") installCmd.Flags().MarkHidden("manifests") rootCmd.AddCommand(installCmd) } func newInstallFlags() installFlags { return installFlags{ logLevel: flags.LogLevel(rootArgs.defaults.LogLevel), defaultComponents: rootArgs.defaults.Components, registry: rootArgs.defaults.Registry, watchAllNamespaces: rootArgs.defaults.WatchAllNamespaces, networkPolicy: rootArgs.defaults.NetworkPolicy, clusterDomain: rootArgs.defaults.ClusterDomain, } } func installCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() components := append(installArgs.defaultComponents, installArgs.extraComponents...) err := utils.ValidateComponents(components) if err != nil { return err } if installArgs.registryCredential != "" && installArgs.imagePullSecret == "" { return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") } if installArgs.registryCredential != "" && len(strings.Split(installArgs.registryCredential, ":")) != 2 { return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") } if ver, err := getVersion(installArgs.version); err != nil { return err } else { installArgs.version = ver } if !installArgs.export { logger.Generatef("generating manifests") } tmpDir, err := manifestgen.MkdirTempAbs("", *kubeconfigArgs.Namespace) if err != nil { return err } defer os.RemoveAll(tmpDir) manifestsBase := "" if isEmbeddedVersion(installArgs.version) { if err := writeEmbeddedManifests(tmpDir); err != nil { return err } manifestsBase = tmpDir } opts := install.Options{ BaseURL: installArgs.manifestsPath, Version: installArgs.version, Namespace: *kubeconfigArgs.Namespace, Components: components, Registry: installArgs.registry, RegistryCredential: installArgs.registryCredential, ImagePullSecret: installArgs.imagePullSecret, WatchAllNamespaces: installArgs.watchAllNamespaces, NetworkPolicy: installArgs.networkPolicy, LogLevel: installArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, ManifestFile: fmt.Sprintf("%s.yaml", *kubeconfigArgs.Namespace), Timeout: rootArgs.timeout, ClusterDomain: installArgs.clusterDomain, TolerationKeys: installArgs.tolerationKeys, } if installArgs.manifestsPath == "" { opts.BaseURL = install.MakeDefaultOptions().BaseURL } manifest, err := install.Generate(opts, manifestsBase) if err != nil { return fmt.Errorf("install failed: %w", err) } if _, err := manifest.WriteFile(tmpDir); err != nil { return fmt.Errorf("install failed: %w", err) } if installArgs.export { _, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content)) return err } else if rootArgs.verbose { _, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content)) if err != nil { return err } } logger.Successf("manifests build completed") logger.Actionf("installing components in %s namespace", *kubeconfigArgs.Namespace) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } installed := true info, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("cluster info unavailable: %w", err) } installed = false } if info.bootstrapped { return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", info.version) } if installed && !installArgs.force { err := confirmFluxInstallOverride(info) if err != nil { if err == promptui.ErrAbort { return fmt.Errorf("installation cancelled") } return err } } applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path)) if err != nil { return fmt.Errorf("install failed: %w", err) } rootCmd.Println(applyOutput) if opts.ImagePullSecret != "" && opts.RegistryCredential != "" { logger.Actionf("generating image pull secret %s", opts.ImagePullSecret) credentials := strings.SplitN(opts.RegistryCredential, ":", 2) secretOpts := sourcesecret.Options{ Name: opts.ImagePullSecret, Namespace: opts.Namespace, Registry: opts.Registry, Username: credentials[0], Password: credentials[1], } imagePullSecret, err := sourcesecret.GenerateOCI(secretOpts) if err != nil { return fmt.Errorf("install failed: %w", err) } var s corev1.Secret if err := yaml.Unmarshal([]byte(imagePullSecret.Content), &s); err != nil { return fmt.Errorf("install failed: %w", err) } if err := upsertSecret(ctx, kubeClient, s); err != nil { return fmt.Errorf("install failed: %w", err) } } kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return fmt.Errorf("install failed: %w", err) } statusChecker, err := status.NewStatusChecker(kubeConfig, 5*time.Second, rootArgs.timeout, logger) if err != nil { return fmt.Errorf("install failed: %w", err) } componentRefs, err := buildComponentObjectRefs(components...) if err != nil { return fmt.Errorf("install failed: %w", err) } logger.Waitingf("verifying installation") if err := statusChecker.Assess(componentRefs...); err != nil { return fmt.Errorf("install failed") } logger.Successf("install finished") return nil } ================================================ FILE: cmd/flux/install_test.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "strings" "testing" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) func TestInstall(t *testing.T) { // The pointer to kubeconfigArgs.Namespace is shared across // the tests. When a new value is set, it will linger and // impact subsequent tests. // Given that this test uses an invalid namespace, it ensures // to restore whatever value it had previously. currentNamespace := *kubeconfigArgs.Namespace t.Cleanup(func() { *kubeconfigArgs.Namespace = currentNamespace }) tests := []struct { name string args string assert assertFunc }{ { name: "invalid namespace", args: "install --namespace='@#[]'", assert: assertError("namespace must be a valid DNS label: \"@#[]\""), }, { name: "invalid sub-command", args: "install unexpectedPosArg --namespace=example", assert: assertError(`unknown command "unexpectedPosArg" for "flux install"`), }, { name: "missing image pull secret", args: "install --registry-creds=fluxcd:test", assert: assertError(`--registry-creds requires --image-pull-secret to be set`), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ args: tt.args, assert: tt.assert, } cmd.runTestCmd(t) }) } } func TestInstall_ComponentsExtra(t *testing.T) { g := NewWithT(t) command := "install --export --components-extra=" + strings.Join(install.MakeDefaultOptions().ComponentsExtra, ",") output, err := executeCommand(command) g.Expect(err).NotTo(HaveOccurred()) manifests, err := ssautil.ReadObjects(strings.NewReader(output)) g.Expect(err).NotTo(HaveOccurred()) foundImageAutomation := false foundImageReflector := false foundSourceWatcher := false foundExternalArtifact := false for _, obj := range manifests { if obj.GetKind() == "Deployment" && obj.GetName() == "image-automation-controller" { foundImageAutomation = true } if obj.GetKind() == "Deployment" && obj.GetName() == "image-reflector-controller" { foundImageReflector = true } if obj.GetKind() == "Deployment" && obj.GetName() == "source-watcher" { foundSourceWatcher = true } if obj.GetKind() == "Deployment" && (obj.GetName() == "kustomize-controller" || obj.GetName() == "helm-controller") { containers, _, _ := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers") g.Expect(containers).ToNot(BeEmpty()) args, _, _ := unstructured.NestedSlice(containers[0].(map[string]any), "args") g.Expect(args).To(ContainElement("--feature-gates=ExternalArtifact=true")) foundExternalArtifact = true } } g.Expect(foundImageAutomation).To(BeTrue(), "image-automation-controller deployment not found") g.Expect(foundImageReflector).To(BeTrue(), "image-reflector-controller deployment not found") g.Expect(foundSourceWatcher).To(BeTrue(), "source-watcher deployment not found") g.Expect(foundExternalArtifact).To(BeTrue(), "ExternalArtifact feature gate not found") } ================================================ FILE: cmd/flux/kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) // kustomizev1.Kustomization var kustomizationType = apiType{ kind: kustomizev1.KustomizationKind, humanKind: "kustomization", groupVersion: kustomizev1.GroupVersion, } type kustomizationAdapter struct { *kustomizev1.Kustomization } func (a kustomizationAdapter) asClientObject() client.Object { return a.Kustomization } func (a kustomizationAdapter) deepCopyClientObject() client.Object { return a.Kustomization.DeepCopy() } // kustomizev1.KustomizationList type kustomizationListAdapter struct { *kustomizev1.KustomizationList } func (a kustomizationListAdapter) asClientList() client.ObjectList { return a.KustomizationList } func (a kustomizationListAdapter) len() int { return len(a.KustomizationList.Items) } ================================================ FILE: cmd/flux/kustomization_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "testing" func TestKustomizationFromGit(t *testing.T) { namespace := allocateNamespace("tkfg") del, err := execSetupTestNamespace(namespace) if err != nil { t.Fatal(err) } t.Cleanup(del) tmpl := map[string]string{"ns": namespace} cases := []struct { args string goldenFile string tmpl map[string]string }{ { "create source git tkfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.3.5", "testdata/kustomization/create_source_git.golden", nil, }, { "create kustomization tkfg --source=tkfg --path=./deploy/overlays/dev --prune=true --interval=5m --health-check=Deployment/frontend.dev --health-check=Deployment/backend.dev --health-check-timeout=3m", "testdata/kustomization/create_kustomization_from_git.golden", nil, }, { "get kustomization tkfg", "testdata/kustomization/get_kustomization_from_git.golden", nil, }, { "reconcile kustomization tkfg --with-source", "testdata/kustomization/reconcile_kustomization_from_git.golden", tmpl, }, { "suspend kustomization tkfg", "testdata/kustomization/suspend_kustomization_from_git.golden", tmpl, }, { "suspend kustomization tkfg foo tkfg bar", "testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden", tmpl, }, { "resume kustomization tkfg foo --wait", "testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden", tmpl, }, { "resume kustomization tkfg", "testdata/kustomization/resume_kustomization_from_git.golden", tmpl, }, { "resume kustomization tkfg tkfg", "testdata/kustomization/resume_kustomization_from_git_multiple_args.golden", tmpl, }, { "delete kustomization tkfg --silent", "testdata/kustomization/delete_kustomization_from_git.golden", tmpl, }, } for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) } } ================================================ FILE: cmd/flux/list.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var listCmd = &cobra.Command{ Use: "list", Short: "List artifacts", Long: `The list command is used for printing the OCI artifacts metadata.`, } func init() { rootCmd.AddCommand(listCmd) } ================================================ FILE: cmd/flux/list_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/google/go-containerregistry/pkg/crane" "github.com/spf13/cobra" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/pkg/printers" ) type listArtifactFlags struct { semverFilter string regexFilter string creds string provider flags.SourceOCIProvider insecure bool } var listArtifactArgs = newListArtifactFlags() func newListArtifactFlags() listArtifactFlags { return listArtifactFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } var listArtifactsCmd = &cobra.Command{ Use: "artifacts", Short: "list artifacts", Long: `The list command fetches the tags and their metadata from a remote OCI repository. The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`, Example: ` # List the artifacts stored in an OCI repository flux list artifacts oci://ghcr.io/org/config/app `, RunE: listArtifactsCmdRun, } func init() { listArtifactsCmd.Flags().StringVar(&listArtifactArgs.semverFilter, "filter-semver", "", "filter tags returned from the oci repository using semver") listArtifactsCmd.Flags().StringVar(&listArtifactArgs.regexFilter, "filter-regex", "", "filter tags returned from the oci repository using regex") listArtifactsCmd.Flags().StringVar(&listArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") listArtifactsCmd.Flags().Var(&listArtifactArgs.provider, "provider", listArtifactArgs.provider.Description()) listArtifactsCmd.Flags().BoolVar(&listArtifactArgs.insecure, "insecure-registry", false, "allows the remote artifacts list to be fetched without TLS") listCmd.AddCommand(listArtifactsCmd) } func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact repository URL is required") } ociURL := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() url, err := oci.ParseArtifactURL(ociURL) if err != nil { return err } ociOpts := oci.DefaultOptions() if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { logger.Actionf("logging in to registry with provider credentials") ociOpt, _, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String()) if err != nil { return fmt.Errorf("error during login with provider: %w", err) } ociOpts = append(ociOpts, ociOpt) } if listArtifactArgs.insecure { ociOpts = append(ociOpts, crane.Insecure) } ociClient := oci.NewClient(ociOpts) if listArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && listArtifactArgs.creds != "" { logger.Actionf("logging in to registry with credentials") if err := ociClient.LoginWithCredentials(listArtifactArgs.creds); err != nil { return fmt.Errorf("could not login with credentials: %w", err) } } opts := oci.ListOptions{ RegexFilter: listArtifactArgs.regexFilter, SemverFilter: listArtifactArgs.semverFilter, } metas, err := ociClient.List(ctx, url, opts) if err != nil { return err } var rows [][]string for _, meta := range metas { rows = append(rows, []string{meta.URL, meta.Digest, meta.Source, meta.Revision}) } err = printers.TablePrinter([]string{"artifact", "digest", "source", "revision"}).Print(cmd.OutOrStdout(), rows) if err != nil { return err } return nil } ================================================ FILE: cmd/flux/log.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "io" ) type stderrLogger struct { stderr io.Writer } func (l stderrLogger) Actionf(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `►`, fmt.Sprintf(format, a...)) } func (l stderrLogger) Generatef(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `✚`, fmt.Sprintf(format, a...)) } func (l stderrLogger) Waitingf(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `◎`, fmt.Sprintf(format, a...)) } func (l stderrLogger) Successf(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `✔`, fmt.Sprintf(format, a...)) } func (l stderrLogger) Warningf(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `⚠️`, fmt.Sprintf(format, a...)) } func (l stderrLogger) Failuref(format string, a ...interface{}) { fmt.Fprintln(l.stderr, `✗`, fmt.Sprintf(format, a...)) } ================================================ FILE: cmd/flux/logs.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "context" "encoding/json" "errors" "fmt" "io" "os" "sort" "strings" "sync" "text/template" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/podutils" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) var logsCmd = &cobra.Command{ Use: "logs", Short: "Display formatted logs for Flux components", Long: withPreviewNote("The logs command displays formatted logs from various Flux components."), Example: ` # Print the reconciliation logs of all Flux custom resources in your cluster flux logs --all-namespaces # Print all logs of all Flux custom resources newer than 2 minutes flux logs --all-namespaces --since=2m # Stream logs for a particular log level flux logs --follow --level=error --all-namespaces # Filter logs by kind, name and namespace flux logs --kind=Kustomization --name=podinfo --namespace=default # Print logs when Flux is installed in a different namespace than flux-system flux logs --flux-namespace=my-namespace `, RunE: logsCmdRun, } type logsFlags struct { logLevel flags.LogLevel follow bool tail int64 kind string name string fluxNamespace string allNamespaces bool sinceTime string sinceDuration time.Duration } var logsArgs = logsFlags{ tail: -1, } const controllerContainer = "manager" func init() { logsCmd.Flags().Var(&logsArgs.logLevel, "level", logsArgs.logLevel.Description()) logsCmd.Flags().StringVarP(&logsArgs.kind, "kind", "", logsArgs.kind, "displays errors of a particular toolkit kind e.g GitRepository") logsCmd.Flags().StringVarP(&logsArgs.name, "name", "", logsArgs.name, "specifies the name of the object logs to be displayed") logsCmd.Flags().BoolVarP(&logsArgs.follow, "follow", "f", logsArgs.follow, "specifies if the logs should be streamed") logsCmd.Flags().Int64VarP(&logsArgs.tail, "tail", "", logsArgs.tail, "lines of recent log file to display") logsCmd.Flags().StringVarP(&logsArgs.fluxNamespace, "flux-namespace", "", rootArgs.defaults.Namespace, "the namespace where the Flux components are running") logsCmd.Flags().BoolVarP(&logsArgs.allNamespaces, "all-namespaces", "A", false, "displays logs for objects across all namespaces") logsCmd.Flags().DurationVar(&logsArgs.sinceDuration, "since", logsArgs.sinceDuration, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") logsCmd.Flags().StringVar(&logsArgs.sinceTime, "since-time", logsArgs.sinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") rootCmd.AddCommand(logsCmd) } func logsCmdRun(cmd *cobra.Command, args []string) error { fluxSelector := fmt.Sprintf("%s=%s", manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return err } clientset, err := kubernetes.NewForConfig(cfg) if err != nil { return err } if len(args) > 0 { return fmt.Errorf("no argument required") } pods, err := getPods(ctx, clientset, logsArgs.fluxNamespace, fluxSelector) if err != nil { return err } logOpts := &corev1.PodLogOptions{ Follow: logsArgs.follow, } if logsArgs.tail > -1 { logOpts.TailLines = &logsArgs.tail } if len(logsArgs.sinceTime) > 0 && logsArgs.sinceDuration != 0 { return fmt.Errorf("at most one of `sinceTime` or `sinceDuration` may be specified") } if len(logsArgs.sinceTime) > 0 { t, err := util.ParseRFC3339(logsArgs.sinceTime, metav1.Now) if err != nil { return fmt.Errorf("%s is not a valid (RFC3339) time", logsArgs.sinceTime) } logOpts.SinceTime = &t } if logsArgs.sinceDuration != 0 { // round up to the nearest second sec := int64(logsArgs.sinceDuration.Round(time.Second).Seconds()) logOpts.SinceSeconds = &sec } var requests []rest.ResponseWrapper for _, pod := range pods { logOpts := logOpts.DeepCopy() if len(pod.Spec.Containers) > 1 { logOpts.Container = controllerContainer } req := clientset.CoreV1().Pods(logsArgs.fluxNamespace).GetLogs(pod.Name, logOpts) requests = append(requests, req) } if logsArgs.follow && len(requests) > 1 { return parallelPodLogs(ctx, requests) } return podLogs(ctx, requests) } // getPods searches for all Deployments in the given namespace that match the given label and returns a list of Pods // from these Deployments. For each Deployment a single Pod is chosen (based on various factors such as the running // state). If no Pod is found, an error is returned. func getPods(ctx context.Context, c *kubernetes.Clientset, ns string, label string) ([]corev1.Pod, error) { var ret []corev1.Pod opts := metav1.ListOptions{ LabelSelector: label, } deployList, err := c.AppsV1().Deployments(ns).List(ctx, opts) if err != nil { return ret, err } for _, deploy := range deployList.Items { label := deploy.Spec.Template.Labels opts := metav1.ListOptions{ LabelSelector: createLabelStringFromMap(label), } podList, err := c.CoreV1().Pods(ns).List(ctx, opts) if err != nil { return ret, err } pods := []*corev1.Pod{} for i := range podList.Items { pod := podList.Items[i] pods = append(pods, &pod) } if len(pods) > 0 { // sort pods to prioritize running pods over others sort.Sort(podutils.ByLogging(pods)) ret = append(ret, *pods[0]) } } if len(ret) == 0 { return nil, fmt.Errorf("no Flux pods found in namespace %q", ns) } return ret, nil } func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error { reader, writer := io.Pipe() errReader, errWriter := io.Pipe() wg := &sync.WaitGroup{} wg.Add(len(requests)) for _, request := range requests { go func(req rest.ResponseWrapper) { defer wg.Done() if err := logRequest(ctx, req, writer); err != nil { fmt.Fprintf(errWriter, "failed getting logs: %s\n", err) return } }(request) } go func() { wg.Wait() writer.Close() errWriter.Close() }() stdoutErrCh := asyncCopy(os.Stdout, reader) stderrErrCh := asyncCopy(os.Stderr, errReader) return errors.Join(<-stdoutErrCh, <-stderrErrCh) } // asyncCopy copies all data from dst to src asynchronously and returns a channel for reading an error value. // This is basically an asynchronous wrapper around `io.Copy`. The returned channel is unbuffered and always is sent // a value (either nil or the error from `io.Copy`) as soon as `io.Copy` returns. // This function lets you copy from multiple sources into multiple destinations in parallel. func asyncCopy(dst io.Writer, src io.Reader) <-chan error { errCh := make(chan error) go func(errCh chan error) { _, err := io.Copy(dst, src) errCh <- err }(errCh) return errCh } func podLogs(ctx context.Context, requests []rest.ResponseWrapper) error { var retErr error for _, req := range requests { if err := logRequest(ctx, req, os.Stdout); err != nil { fmt.Fprintf(os.Stderr, "failed getting logs: %s\n", err) retErr = fmt.Errorf("failed to collect logs from all Flux pods") continue } } return retErr } func createLabelStringFromMap(m map[string]string) string { var strArr []string for key, val := range m { pair := fmt.Sprintf("%v=%v", key, val) strArr = append(strArr, pair) } return strings.Join(strArr, ",") } func logRequest(ctx context.Context, request rest.ResponseWrapper, w io.Writer) error { stream, err := request.Stream(ctx) if err != nil { return err } defer stream.Close() scanner := bufio.NewScanner(stream) const logTmpl = "{{.Timestamp}} {{.Level}} {{or .Kind .ControllerKind}}{{if .Name}}/{{.Name}}.{{.Namespace}}{{end}} - {{.Message}} {{.Error}}\n" t, err := template.New("log").Parse(logTmpl) if err != nil { return fmt.Errorf("unable to create template, err: %s", err) } bw := bufio.NewWriter(w) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "{") { continue } var l ControllerLogEntry if err := json.Unmarshal([]byte(line), &l); err != nil { logger.Failuref("parse error: %s", err) break } filterPrintLog(t, &l, bw) bw.Flush() } return nil } func filterPrintLog(t *template.Template, l *ControllerLogEntry, w io.Writer) { if (logsArgs.logLevel == "" || logsArgs.logLevel == l.Level) && (logsArgs.kind == "" || strings.EqualFold(logsArgs.kind, l.Kind) || strings.EqualFold(logsArgs.kind, l.ControllerKind)) && (logsArgs.name == "" || strings.EqualFold(logsArgs.name, l.Name)) && (logsArgs.allNamespaces || strings.EqualFold(*kubeconfigArgs.Namespace, l.Namespace)) { err := t.Execute(w, l) if err != nil { logger.Failuref("log template error: %s", err) } } } type ControllerLogEntry struct { Timestamp string `json:"ts"` Level flags.LogLevel `json:"level"` Message string `json:"msg"` Error string `json:"error,omitempty"` Logger string `json:"logger"` Kind string `json:"reconciler kind,omitempty"` ControllerKind string `json:"controllerKind,omitempty"` Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` } ================================================ FILE: cmd/flux/logs_e2e_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestLogsNoArgs(t *testing.T) { cmd := cmdTestCase{ args: "logs", assert: assertSuccess(), } cmd.runTestCmd(t) } func TestLogsWrongNamespace(t *testing.T) { cmd := cmdTestCase{ args: "logs --flux-namespace=default", assert: assertError(`no Flux pods found in namespace "default"`), } cmd.runTestCmd(t) } func TestLogsAllNamespaces(t *testing.T) { cmd := cmdTestCase{ args: "logs --all-namespaces", assert: assertSuccess(), } cmd.runTestCmd(t) } func TestLogsSince(t *testing.T) { cmd := cmdTestCase{ args: "logs --since=2m", assert: assertSuccess(), } cmd.runTestCmd(t) } func TestLogsSinceInvalid(t *testing.T) { cmd := cmdTestCase{ args: "logs --since=XXX", assert: assertError(`invalid argument "XXX" for "--since" flag: time: invalid duration "XXX"`), } cmd.runTestCmd(t) } func TestLogsSinceTime(t *testing.T) { cmd := cmdTestCase{ args: "logs --since-time=2021-08-06T14:26:25.546Z", assert: assertSuccess(), } cmd.runTestCmd(t) } func TestLogsSinceTimeInvalid(t *testing.T) { cmd := cmdTestCase{ args: "logs --since-time=XXX", assert: assertError("XXX is not a valid (RFC3339) time"), } cmd.runTestCmd(t) } func TestLogsSinceOnlyOneAllowed(t *testing.T) { cmd := cmdTestCase{ args: "logs --since=2m --since-time=2021-08-06T14:26:25.546Z", assert: assertError("at most one of `sinceTime` or `sinceDuration` may be specified"), } cmd.runTestCmd(t) } ================================================ FILE: cmd/flux/logs_unit_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "context" "io" "os" "strings" "testing" . "github.com/onsi/gomega" ) func TestLogRequest(t *testing.T) { mapper := &testResponseMapper{} tests := []struct { name string namespace string flags logsFlags assertFile string }{ { name: "all logs", flags: logsFlags{ tail: -1, allNamespaces: true, }, assertFile: "testdata/logs/all-logs.txt", }, { name: "filter by namespace", namespace: "default", flags: logsFlags{ tail: -1, }, assertFile: "testdata/logs/namespace.txt", }, { name: "filter by kind and namespace", flags: logsFlags{ tail: -1, kind: "Kustomization", }, assertFile: "testdata/logs/kind.txt", }, { name: "filter by loglevel", flags: logsFlags{ tail: -1, logLevel: "error", allNamespaces: true, }, assertFile: "testdata/logs/log-level.txt", }, { name: "filter by namespace, name, loglevel and kind", namespace: "flux-system", flags: logsFlags{ tail: -1, logLevel: "error", kind: "Kustomization", name: "podinfo", }, assertFile: "testdata/logs/multiple-filters.txt", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) logsArgs = tt.flags if tt.namespace != "" { *kubeconfigArgs.Namespace = tt.namespace } w := bytes.NewBuffer([]byte{}) err := logRequest(context.Background(), mapper, w) g.Expect(err).To(BeNil()) got := make([]byte, w.Len()) _, err = w.Read(got) g.Expect(err).To(BeNil()) expected, err := os.ReadFile(tt.assertFile) g.Expect(err).To(BeNil()) g.Expect(string(got)).To(Equal(string(expected))) // reset flags to default *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace logsArgs = logsFlags{ tail: -1, } }) } } var testPodLogs = `{"level":"info","ts":"2022-08-02T12:55:34.419Z","msg":"no changes since last reconcilation: observed revision","controller":"gitrepository","controllerGroup":"source.toolkit.fluxcd.io","controllerKind":"GitRepository","gitRepository":{"name":"podinfo","namespace":"default"},"namespace":"default","name":"podinfo","reconcileID":"5ef9b2ef-4ea5-47b7-b887-a247cafc1bce"} {"level":"error","ts":"2022-08-02T12:56:04.679Z","logger":"controller.gitrepository","msg":"no changes since last reconcilation: observed revision","controllerGroup":"source.toolkit.fluxcd.io","controllerKind":"GitRepository","gitRepository":{"name":"podinfo","namespace":"flux-system"},"name":"flux-system","namespace":"flux-system","reconcileID":"543ef9b2ef-4ea5-47b7-b887-a247cafc1bce"} {"level":"error","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"flux-system","namespace":"flux-system"} {"level":"info","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"podinfo","namespace":"default"} {"level":"info","ts":"2022-08-02T12:56:34.961Z","logger":"controller.gitrepository","msg":"no changes since last reconcilation: observed revision","reconciler group":"source.toolkit.fluxcd.io","reconciler kind":"GitRepository","name":"podinfo","namespace":"default"} {"level":"error","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"podinfo","namespace":"flux-system"} ` type testResponseMapper struct { } func (t *testResponseMapper) DoRaw(_ context.Context) ([]byte, error) { return nil, nil } func (t *testResponseMapper) Stream(_ context.Context) (io.ReadCloser, error) { return io.NopCloser(strings.NewReader(testPodLogs)), nil } ================================================ FILE: cmd/flux/main.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "fmt" "log" "os" "strings" "time" "github.com/go-logr/logr" "github.com/spf13/cobra" "golang.org/x/term" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) var VERSION = "0.0.0-dev.0" var rootCmd = &cobra.Command{ Use: "flux", Version: VERSION, SilenceUsage: true, SilenceErrors: true, Short: "Command line utility for assembling Kubernetes CD pipelines", Long: ` Command line utility for assembling Kubernetes CD pipelines the GitOps way.`, Example: ` # Check prerequisites flux check --pre # Install the latest version of Flux flux install # Create a source for a public Git repository flux create source git webapp-latest \ --url=https://github.com/stefanprodan/podinfo \ --branch=master \ --interval=3m # List GitRepository sources and their status flux get sources git # Trigger a GitRepository source reconciliation flux reconcile source git flux-system # Export GitRepository sources in YAML format flux export source git --all > sources.yaml # Create a Kustomization for deploying a series of microservices flux create kustomization webapp-dev \ --source=webapp-latest \ --path="./deploy/webapp/" \ --prune=true \ --interval=5m \ --health-check="Deployment/backend.webapp" \ --health-check="Deployment/frontend.webapp" \ --health-check-timeout=2m # Trigger a git sync of the Kustomization's source and apply changes flux reconcile kustomization webapp-dev --with-source # Suspend a Kustomization reconciliation flux suspend kustomization webapp-dev # Export Kustomizations in YAML format flux export kustomization --all > kustomizations.yaml # Resume a Kustomization reconciliation flux resume kustomization webapp-dev # Delete a Kustomization flux delete kustomization webapp-dev # Delete a GitRepository source flux delete source git webapp-latest # Uninstall Flux and delete CRDs flux uninstall`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { ns, err := cmd.Flags().GetString("namespace") if err != nil { return fmt.Errorf("error getting namespace: %w", err) } if e := validation.IsDNS1123Label(ns); len(e) > 0 { return fmt.Errorf("namespace must be a valid DNS label: %q", ns) } return nil }, } var logger = stderrLogger{stderr: os.Stderr} type rootFlags struct { timeout time.Duration verbose bool pollInterval time.Duration 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) var kubeclientOptions = new(runclient.Options) func init() { rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation") rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects") configureDefaultNamespace() kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag kubeconfigArgs.Timeout = nil // prevent AddFlags from configuring --request-timeout flag, we have --timeout instead kubeconfigArgs.AddFlags(rootCmd.PersistentFlags()) // Since some subcommands use the `-s` flag as a short version for `--silent`, we manually configure the server flag // without the `-s` short version. While we're no longer on par with kubectl's flags, we maintain backwards compatibility // on the CLI interface. apiServer := "" kubeconfigArgs.APIServer = &apiServer rootCmd.PersistentFlags().StringVar(kubeconfigArgs.APIServer, "server", *kubeconfigArgs.APIServer, "The address and port of the Kubernetes API server") // Update the description for kubeconfig TLS flags so that user's don't mistake it for a Flux specific flag rootCmd.Flag("insecure-skip-tls-verify").Usage = "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure" rootCmd.Flag("client-certificate").Usage = "Path to a client certificate file for TLS authentication to the Kubernetes API server" rootCmd.Flag("certificate-authority").Usage = "Path to a cert file for the certificate authority to authenticate the Kubernetes API server" rootCmd.Flag("client-key").Usage = "Path to a client key file for TLS authentication to the Kubernetes API server" kubeclientOptions.BindFlags(rootCmd.PersistentFlags()) rootCmd.RegisterFlagCompletionFunc("context", contextsCompletionFunc) rootCmd.RegisterFlagCompletionFunc("namespace", resourceNamesCompletionFunc(corev1.SchemeGroupVersion.WithKind("Namespace"))) rootCmd.DisableAutoGenTag = true rootCmd.SetOut(os.Stdout) } func NewRootFlags() rootFlags { rf := rootFlags{ pollInterval: 2 * time.Second, defaults: install.MakeDefaultOptions(), } rf.defaults.Version = "v" + VERSION return rf } func main() { log.SetFlags(0) // This is required because controller-runtime expects its consumers to // set a logger through log.SetLogger within 30 seconds of the program's // initialization. If not set, the entire debug stack is printed as an // error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59 // Since we have our own logging and don't care about controller-runtime's // logger, we configure it's logger to do nothing. ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{})) 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) } } func configureDefaultNamespace() { *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE") if fromEnv != "" { // namespace must be a valid DNS label. Assess against validation // used upstream, and ignore invalid values as environment vars // may not be actively provided by end-user. if e := validation.IsDNS1123Label(fromEnv); len(e) > 0 { logger.Warningf(" ignoring invalid FLUX_SYSTEM_NAMESPACE: %q", fromEnv) return } kubeconfigArgs.Namespace = &fromEnv } } // readPasswordFromStdin reads a password from stdin and returns the input // with trailing newline and/or carriage return removed. It also makes sure that terminal // echoing is turned off if stdin is a terminal. func readPasswordFromStdin(prompt string) (string, error) { var out string var err error if _, err := fmt.Fprint(os.Stdout, prompt); err != nil { return "", fmt.Errorf("failed to write prompt: %w", err) } stdinFD := int(os.Stdin.Fd()) if term.IsTerminal(stdinFD) { var inBytes []byte inBytes, err = term.ReadPassword(int(os.Stdin.Fd())) out = string(inBytes) } else { out, err = bufio.NewReader(os.Stdin).ReadString('\n') } if err != nil { return "", fmt.Errorf("could not read from stdin: %w", err) } fmt.Println() return strings.TrimRight(out, "\r\n"), nil } func withPreviewNote(desc string) string { previewNote := `⚠️ Please note that this command is in preview and under development. While we try our best to not introduce breaking changes, they may occur when we adapt to new features and/or find better ways to facilitate what it does.` return fmt.Sprintf("%s\n\n%s", strings.TrimSpace(desc), previewNote) } // printlnStdout prints the given text to stdout with a newline. func printlnStdout(txt string) { _, _ = rootCmd.OutOrStdout().Write([]byte(txt + "\n")) } ================================================ FILE: cmd/flux/main_e2e_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "testing" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/fluxcd/flux2/v2/internal/utils" ) func TestMain(m *testing.M) { log.SetLogger(logr.New(log.NullLogSink{})) // Ensure tests print consistent timestamps regardless of timezone os.Setenv("TZ", "UTC") testEnv, err := NewTestEnvKubeManager(ExistingClusterMode) if err != nil { panic(fmt.Errorf("error creating kube manager: '%w'", err)) } kubeconfigArgs.KubeConfig = &testEnv.kubeConfigPath // Install Flux. output, err := executeCommand("install --components-extra=image-reflector-controller,image-automation-controller") if err != nil { panic(fmt.Errorf("install failed: %s error:'%w'", output, err)) } // Run tests code := m.Run() // Uninstall Flux output, err = executeCommand("uninstall -s --keep-namespace") if err != nil { panic(fmt.Errorf("uninstall failed: %s error:'%w'", output, err)) } // Delete namespace and wait for finalisation kubectlArgs := []string{"delete", "namespace", "flux-system"} _, err = utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) if err != nil { panic(fmt.Errorf("delete namespace error:'%w'", err)) } testEnv.Stop() os.Exit(code) } func execSetupTestNamespace(namespace string) (func(), error) { kubectlArgs := []string{"create", "namespace", namespace} _, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) if err != nil { return nil, err } return func() { kubectlArgs := []string{"delete", "namespace", namespace} utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) }, nil } ================================================ FILE: cmd/flux/main_test.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "bytes" "context" "flag" "fmt" "io" "os" "path/filepath" "strings" "sync/atomic" "testing" "text/template" "time" "github.com/google/go-cmp/cmp" "github.com/mattn/go-shellwords" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "github.com/fluxcd/flux2/v2/internal/utils" ) var nextNamespaceId int64 // update allows golden files to be updated based on the current output. var update = flag.Bool("update", false, "update golden files") // Return a unique namespace with the specified prefix, for tests to create // objects that won't collide with each other. func allocateNamespace(prefix string) string { id := atomic.AddInt64(&nextNamespaceId, 1) return fmt.Sprintf("%s-%d", prefix, id) } func readYamlObjects(rdr io.Reader) ([]*unstructured.Unstructured, error) { objects := []*unstructured.Unstructured{} reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr)) for { doc, err := reader.Read() if err != nil { if err == io.EOF { break } } unstructuredObj := &unstructured.Unstructured{} decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(doc), len(doc)) err = decoder.Decode(unstructuredObj) if err != nil { return nil, err } objects = append(objects, unstructuredObj) } return objects, nil } // A KubeManager that can create objects that are subject to a test. type testEnvKubeManager struct { client client.WithWatch testEnv *envtest.Environment kubeConfigPath string } func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues map[string]string, t *testing.T) { buf, err := os.ReadFile(objectFile) if err != nil { t.Fatalf("Error reading file '%s': %v", objectFile, err) } content, err := executeTemplate(string(buf), templateValues) if err != nil { t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) } clientObjects, err := readYamlObjects(strings.NewReader(content)) if err != nil { t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) } err = m.CreateObjects(clientObjects, t) if err != nil { t.Logf("Error creating test objects: '%v'", err) } } func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { for _, obj := range clientObjects { // First create the object then set its status if present in the // yaml file. Make a copy first since creating an object may overwrite // the status. createObj := obj.DeepCopy() err := m.client.Create(context.Background(), createObj) if err != nil { return err } obj.SetResourceVersion(createObj.GetResourceVersion()) err = m.client.Status().Update(context.Background(), obj) // Updating status of static objects results in not found error. if err != nil && !errors.IsNotFound(err) { return err } } return nil } func (m *testEnvKubeManager) DeleteObjectFile(objectFile string, templateValues map[string]string, t *testing.T) { buf, err := os.ReadFile(objectFile) if err != nil { t.Fatalf("Error reading file '%s': %v", objectFile, err) } content, err := executeTemplate(string(buf), templateValues) if err != nil { t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) } clientObjects, err := readYamlObjects(strings.NewReader(content)) if err != nil { t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) } err = m.DeleteObjects(clientObjects, t) if err != nil { t.Logf("Error deleting test objects: '%v'", err) } } func (m *testEnvKubeManager) DeleteObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { for _, obj := range clientObjects { err := m.client.Delete(context.Background(), obj) if err != nil { return err } } return nil } func (m *testEnvKubeManager) Stop() error { if m.testEnv == nil { return fmt.Errorf("do nothing because testEnv is nil") } return m.testEnv.Stop() } func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager, error) { switch testClusterMode { case TestEnvClusterMode: useExistingCluster := false testEnv := &envtest.Environment{ UseExistingCluster: &useExistingCluster, CRDDirectoryPaths: []string{"manifests"}, } cfg, err := testEnv.Start() if err != nil { return nil, err } user, err := testEnv.ControlPlane.AddUser(envtest.User{ Name: "envtest-admin", Groups: []string{"system:masters"}, }, nil) if err != nil { return nil, err } kubeConfig, err := user.KubeConfig() if err != nil { return nil, err } tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String()) os.WriteFile(tmpFilename, kubeConfig, 0o600) k8sClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: utils.NewScheme(), }) if err != nil { return nil, err } return &testEnvKubeManager{ testEnv: testEnv, client: k8sClient, kubeConfigPath: tmpFilename, }, nil case ExistingClusterMode: // TEST_KUBECONFIG is mandatory to prevent destroying a current cluster accidentally. testKubeConfig := os.Getenv("TEST_KUBECONFIG") if testKubeConfig == "" { return nil, fmt.Errorf("environment variable TEST_KUBECONFIG is required to run tests against an existing cluster") } useExistingCluster := true config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig) if err != nil { return nil, err } testEnv := &envtest.Environment{ UseExistingCluster: &useExistingCluster, Config: config, } cfg, err := testEnv.Start() if err != nil { return nil, err } k8sClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: utils.NewScheme(), }) if err != nil { return nil, err } return &testEnvKubeManager{ testEnv: testEnv, client: k8sClient, kubeConfigPath: testKubeConfig, }, nil } return nil, nil } // Function that sets an expectation on the output of a command. Tests can // either implement this directly or use a helper below. type assertFunc func(output string, err error) error // Assemble multiple assertFuncs into a single assertFunc func assert(fns ...assertFunc) assertFunc { return func(output string, err error) error { for _, fn := range fns { if assertErr := fn(output, err); assertErr != nil { return assertErr } } return nil } } // Expect the command to run without error func assertSuccess() assertFunc { return func(output string, err error) error { if err != nil { return fmt.Errorf("Expected success but was error: %v", err) } return nil } } // Expect the command to fail with the specified error func assertError(expected string) assertFunc { return func(output string, err error) error { if err == nil { return fmt.Errorf("Expected error but was success") } if expected != err.Error() { return fmt.Errorf("Expected error '%v' but got '%v'", expected, err.Error()) } return nil } } // Expect the command to succeed with the expected test output. func assertGoldenValue(expected string) assertFunc { return assert( assertSuccess(), func(output string, err error) error { diff := cmp.Diff(expected, output) if diff != "" { return fmt.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } return nil }) } // Filename that contains the expected test output. func assertGoldenFile(goldenFile string) assertFunc { return assertGoldenTemplateFile(goldenFile, map[string]string{}) } // Filename that contains the expected test output. The golden file is a template that // is pre-processed with the specified templateValues. func assertGoldenTemplateFile(goldenFile string, templateValues map[string]string) assertFunc { goldenFileContents, fileErr := os.ReadFile(goldenFile) return assert( assertSuccess(), func(output string, err error) error { if fileErr != nil { return fmt.Errorf("Error reading golden file '%s': %s", goldenFile, fileErr) } var expectedOutput string if len(templateValues) > 0 { expectedOutput, err = executeTemplate(string(goldenFileContents), templateValues) if err != nil { return fmt.Errorf("Error executing golden template file '%s': %s", goldenFile, err) } } else { expectedOutput = string(goldenFileContents) } if assertErr := assertGoldenValue(expectedOutput)(output, err); assertErr != nil { // Update the golden files if comparison fails and the update flag is set. if *update && output != "" { // Skip update if there are template values. if len(templateValues) > 0 { fmt.Println("NOTE: -update flag passed but golden template files can't be updated, please update it manually") } else { if err := os.WriteFile(goldenFile, []byte(output), 0o600); err != nil { return fmt.Errorf("failed to update golden file '%s': %v", goldenFile, err) } return nil } } return fmt.Errorf("Mismatch from golden file '%s': %v", goldenFile, assertErr) } return nil }) } type TestClusterMode int const ( TestEnvClusterMode = TestClusterMode(iota + 1) ExistingClusterMode ) // Structure used for each test to load objects into kubernetes, run // commands and assert on the expected output. type cmdTestCase struct { // The command line arguments to test. args string // Tests use assertFunc to assert on an output, success or failure. This // can be a function defined by the test or existing function above. assert assertFunc } 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) } } func executeTemplate(content string, templateValues map[string]string) (string, error) { tmpl := template.Must(template.New("golden").Parse(content)) var out bytes.Buffer if err := tmpl.Execute(&out, templateValues); err != nil { return "", err } return out.String(), nil } // Run the command and return the captured output. func executeCommand(cmd string) (string, error) { defer resetCmdArgs() defer func() { // need to set this explicitly because apparently its value isn't changed // in subsequent executions which causes tests to fail that rely on the value // of "Changed". resumeCmd.PersistentFlags().Lookup("wait").Changed = false }() args, err := shellwords.Parse(cmd) if err != nil { return "", err } buf := new(bytes.Buffer) rootCmd.SetOut(buf) rootCmd.SetErr(buf) rootCmd.SetArgs(args) logger.stderr = rootCmd.ErrOrStderr() _, err = rootCmd.ExecuteC() result := buf.String() return result, err } // Run the command while passing the string as input and return the captured output. func executeCommandWithIn(cmd string, in io.Reader) (string, error) { defer resetCmdArgs() args, err := shellwords.Parse(cmd) if err != nil { return "", err } buf := new(bytes.Buffer) rootCmd.SetOut(buf) rootCmd.SetErr(buf) rootCmd.SetArgs(args) if in != nil { rootCmd.SetIn(in) } _, err = rootCmd.ExecuteC() result := buf.String() return result, err } // resetCmdArgs resets the flags for various cmd // Note: this will also clear default value of the flags set in init() func resetCmdArgs() { *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace alertArgs = alertFlags{} alertProviderArgs = alertProviderFlags{} bootstrapArgs = NewBootstrapFlags() bServerArgs = bServerFlags{} logsArgs = logsFlags{ tail: -1, fluxNamespace: rootArgs.defaults.Namespace, } buildKsArgs = buildKsFlags{ localSources: map[string]string{}, } checkArgs = checkFlags{} createArgs = createFlags{} deleteArgs = deleteFlags{} diffKsArgs = diffKsFlags{} exportArgs = exportFlags{} getArgs = GetFlags{} gitArgs = gitFlags{} githubArgs = githubFlags{} gitlabArgs = gitlabFlags{} helmReleaseArgs = helmReleaseFlags{ reconcileStrategy: "ChartVersion", } imagePolicyArgs = imagePolicyFlags{} imageRepoArgs = imageRepoFlags{} imageUpdateArgs = imageUpdateFlags{} installArgs = newInstallFlags() kustomizationArgs = NewKustomizationFlags() receiverArgs = receiverFlags{} resumeArgs = ResumeFlags{} rhrArgs = reconcileHelmReleaseFlags{} rksArgs = reconcileKsFlags{} secretGitArgs = NewSecretGitFlags() secretGitHubAppArgs = secretGitHubAppFlags{} secretProxyArgs = secretProxyFlags{} secretHelmArgs = secretHelmFlags{} secretTLSArgs = secretTLSFlags{} sourceBucketArgs = sourceBucketFlags{} sourceGitArgs = newSourceGitFlags() sourceHelmArgs = sourceHelmFlags{} sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} suspendArgs = SuspendFlags{} tenantArgs = tenantFlags{} traceArgs = traceFlags{} treeKsArgs = TreeKsFlags{} uninstallArgs = uninstallFlags{} versionArgs = versionFlags{ output: "yaml", } envsubstArgs = envsubstFlags{} debugHelmReleaseArgs = debugHelmReleaseFlags{} debugKustomizationArgs = debugKustomizationFlags{} } 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 } ================================================ FILE: cmd/flux/main_unit_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "testing" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/log" ) // The test environment is long running process shared between tests, initialized // by a `TestMain` function depending on how the test is involved and which tests // are a part of the build. var testEnv *testEnvKubeManager func TestMain(m *testing.M) { log.SetLogger(logr.New(log.NullLogSink{})) // Ensure tests print consistent timestamps regardless of timezone os.Setenv("TZ", "UTC") // Creating the test env manager sets rootArgs client flags km, err := NewTestEnvKubeManager(TestEnvClusterMode) if err != nil { panic(fmt.Errorf("error creating kube manager: '%w'", err)) } testEnv = km // rootArgs.kubeconfig = testEnv.kubeConfigPath kubeconfigArgs.KubeConfig = &testEnv.kubeConfigPath // Run tests code := m.Run() km.Stop() os.Exit(code) } func setupTestNamespace(namespace string, t *testing.T) { ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} err := testEnv.client.Create(context.Background(), ns) if err != nil { t.Fatalf("Failed to create namespace: %v", err) } t.Cleanup(func() { _ = testEnv.client.Delete(context.Background(), ns) }) } ================================================ FILE: cmd/flux/manifests.embed.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "embed" "fmt" "io/fs" "os" "path" ) //go:embed manifests/*.yaml var embeddedManifests embed.FS func writeEmbeddedManifests(dir string) error { manifests, err := fs.ReadDir(embeddedManifests, "manifests") if err != nil { return err } for _, manifest := range manifests { data, err := fs.ReadFile(embeddedManifests, path.Join("manifests", manifest.Name())) if err != nil { return fmt.Errorf("reading file failed: %w", err) } err = os.WriteFile(path.Join(dir, manifest.Name()), data, 0666) if err != nil { return fmt.Errorf("writing file failed: %w", err) } } return nil } ================================================ FILE: cmd/flux/migrate.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/fluxcd/pkg/ssa" "github.com/manifoldco/promptui" "github.com/spf13/cobra" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" imageautov1 "github.com/fluxcd/image-automation-controller/api/v1" imageautov1b2 "github.com/fluxcd/image-automation-controller/api/v1beta2" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" imagev1b2 "github.com/fluxcd/image-reflector-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" sourcev1 "github.com/fluxcd/source-controller/api/v1" swv1b1 "github.com/fluxcd/source-watcher/api/v2/v1beta1" "github.com/fluxcd/flux2/v2/internal/utils" ) // APIVersions holds the mapping of GroupKinds to their respective // latest API versions for a specific Flux version. type APIVersions struct { FluxVersion string LatestVersions map[schema.GroupKind]string } // TODO: Update this mapping when new Flux minor versions are released! // latestAPIVersions contains the latest API versions for each GroupKind // for each supported Flux version. The number of latest minor versions // we maintain here must match what's documented here: // // https://fluxcd.io/flux/releases/#supported-releases var latestAPIVersions = []APIVersions{ { FluxVersion: "2.8", LatestVersions: flux27LatestAPIVersions, }, { FluxVersion: "2.7", LatestVersions: flux27LatestAPIVersions, }, { FluxVersion: "2.6", LatestVersions: flux26LatestAPIVersions, }, } var flux27LatestAPIVersions = map[schema.GroupKind]string{ // source-controller {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version, // kustomize-controller {Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version, // helm-controller {Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version, // notification-controller {Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version, {Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version, {Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version, // image-reflector-controller {Group: imagev1.GroupVersion.Group, Kind: imagev1.ImageRepositoryKind}: imagev1.GroupVersion.Version, {Group: imagev1.GroupVersion.Group, Kind: imagev1.ImagePolicyKind}: imagev1.GroupVersion.Version, // image-automation-controller {Group: imageautov1.GroupVersion.Group, Kind: imageautov1.ImageUpdateAutomationKind}: imageautov1.GroupVersion.Version, // source-watcher {Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version, } var flux26LatestAPIVersions = map[schema.GroupKind]string{ // source-controller {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.OCIRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmRepositoryKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.HelmChartKind}: sourcev1.GroupVersion.Version, {Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}: sourcev1.GroupVersion.Version, // kustomize-controller {Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}: kustomizev1.GroupVersion.Version, // helm-controller {Group: helmv2.GroupVersion.Group, Kind: helmv2.HelmReleaseKind}: helmv2.GroupVersion.Version, // notification-controller {Group: notificationv1.GroupVersion.Group, Kind: notificationv1.ReceiverKind}: notificationv1.GroupVersion.Version, {Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.AlertKind}: notificationv1b3.GroupVersion.Version, {Group: notificationv1b3.GroupVersion.Group, Kind: notificationv1b3.ProviderKind}: notificationv1b3.GroupVersion.Version, // image-reflector-controller {Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImageRepositoryKind}: imagev1b2.GroupVersion.Version, {Group: imagev1b2.GroupVersion.Group, Kind: imagev1b2.ImagePolicyKind}: imagev1b2.GroupVersion.Version, // image-automation-controller {Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version, } var migrateCmd = &cobra.Command{ Use: "migrate", Args: cobra.NoArgs, Short: "Migrate the Flux custom resources to their latest API version", Long: `The migrate command must be run before a Flux minor version upgrade. The command has two modes of operation: - Cluster mode (default): migrates all the Flux custom resources stored in Kubernetes etcd to their latest API version. - File system mode (-f): migrates the Flux custom resources defined in the manifests located in the specified path. `, Example: ` # Migrate all the Flux custom resources in the cluster. # This uses the current kubeconfig context and requires cluster-admin permissions. flux migrate # Migrate all the Flux custom resources in a Git repository # checked out in the current working directory. flux migrate -f . # Migrate all Flux custom resources defined in YAML and Helm YAML template files. flux migrate -f . --extensions=.yml,.yaml,.tpl # Migrate the Flux custom resources to the latest API versions of Flux 2.6. flux migrate -f . --version=2.6 # Migrate the Flux custom resources defined in a multi-document YAML manifest file. flux migrate -f path/to/manifest.yaml # Simulate the migration without making any changes. flux migrate -f . --dry-run # Run the migration skipping confirmation prompts. flux migrate -f . --yes `, RunE: runMigrateCmd, } var migrateFlags struct { yes bool dryRun bool path string version string extensions []string } func init() { rootCmd.AddCommand(migrateCmd) migrateCmd.Flags().StringVarP(&migrateFlags.path, "path", "f", "", "the path to the directory containing the manifests to migrate") migrateCmd.Flags().StringSliceVarP(&migrateFlags.extensions, "extensions", "e", []string{".yaml", ".yml"}, "the file extensions to consider when migrating manifests, only applicable with --path") migrateCmd.Flags().StringVarP(&migrateFlags.version, "version", "v", "", "the target Flux minor version to migrate manifests to, only applicable with --path (defaults to the version of the CLI)") migrateCmd.Flags().BoolVarP(&migrateFlags.yes, "yes", "y", false, "skip confirmation prompts when migrating manifests, only applicable with --path") migrateCmd.Flags().BoolVar(&migrateFlags.dryRun, "dry-run", false, "simulate the migration of manifests without making any changes, only applicable with --path") } func runMigrateCmd(*cobra.Command, []string) error { if migrateFlags.path == "" { return migrateCluster() } return migrateFileSystem() } func migrateCluster() error { logger.Actionf("starting migration of custom resources") ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return fmt.Errorf("the Kubernetes client initialization failed: %w", err) } kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()}) if err != nil { return err } migrator := NewClusterMigrator(kubeClient, client.MatchingLabels{ "app.kubernetes.io/part-of": "flux", }) if err := migrator.Run(ctx); err != nil { return err } logger.Successf("custom resources migrated successfully") return nil } func migrateFileSystem() error { pathRoot, err := os.OpenRoot(".") if err != nil { return fmt.Errorf("failed to open filesystem at the current working directory: %w", err) } defer pathRoot.Close() fileSystem := &osFS{pathRoot.FS()} yes := migrateFlags.yes dryRun := migrateFlags.dryRun path := migrateFlags.path extensions := migrateFlags.extensions var latestVersions map[schema.GroupKind]string // Determine latest API versions based on the Flux version. if migrateFlags.version == "" { latestVersions = latestAPIVersions[0].LatestVersions } else { supportedVersions := make([]string, 0, len(latestAPIVersions)) for _, v := range latestAPIVersions { if v.FluxVersion == migrateFlags.version { latestVersions = v.LatestVersions break } supportedVersions = append(supportedVersions, v.FluxVersion) } if latestVersions == nil { return fmt.Errorf("version %s is not supported, supported versions are: %s", migrateFlags.version, strings.Join(supportedVersions, ", ")) } } return NewFileSystemMigrator(fileSystem, yes, dryRun, path, extensions, latestVersions).Run() } // ClusterMigrator migrates all the CRs in the cluster for the CRDs matching the label selector. type ClusterMigrator struct { labelSelector client.MatchingLabels kubeClient client.Client } // NewClusterMigrator creates a new ClusterMigrator instance with the specified label selector. func NewClusterMigrator(kubeClient client.Client, labelSelector client.MatchingLabels) *ClusterMigrator { return &ClusterMigrator{ labelSelector: labelSelector, kubeClient: kubeClient, } } func (c *ClusterMigrator) Run(ctx context.Context) error { crdList := &apiextensionsv1.CustomResourceDefinitionList{} if err := c.kubeClient.List(ctx, crdList, c.labelSelector); err != nil { return fmt.Errorf("failed to list CRDs: %w", err) } for _, crd := range crdList.Items { if err := c.migrateCRD(ctx, crd.Name); err != nil { return err } } return nil } func (c *ClusterMigrator) migrateCRD(ctx context.Context, name string) error { crd := &apiextensionsv1.CustomResourceDefinition{} if err := c.kubeClient.Get(ctx, client.ObjectKey{Name: name}, crd); err != nil { return fmt.Errorf("failed to get CRD %s: %w", name, err) } // get the latest storage version for the CRD storageVersion := c.getStorageVersion(crd) if storageVersion == "" { return fmt.Errorf("no storage version found for CRD %s", name) } // migrate all the resources for the CRD err := retry.RetryOnConflict(retry.DefaultRetry, func() error { return c.migrateCR(ctx, crd, storageVersion) }) if err != nil { return fmt.Errorf("failed to migrate resources for CRD %s: %w", name, err) } // set the CRD status to contain only the latest storage version if len(crd.Status.StoredVersions) > 1 || crd.Status.StoredVersions[0] != storageVersion { crd.Status.StoredVersions = []string{storageVersion} if err := c.kubeClient.Status().Update(ctx, crd); err != nil { return fmt.Errorf("failed to update CRD %s status: %w", crd.Name, err) } logger.Successf("%s migrated to storage version %s", crd.Name, storageVersion) } return nil } // migrateCR migrates all CRs for the given CRD to the specified version by patching them. func (c *ClusterMigrator) migrateCR(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, version string) error { list := &unstructured.UnstructuredList{} apiVersion := crd.Spec.Group + "/" + version listKind := crd.Spec.Names.ListKind list.SetAPIVersion(apiVersion) list.SetKind(listKind) err := c.kubeClient.List(ctx, list, client.InNamespace("")) if err != nil { return fmt.Errorf("failed to list resources for CRD %s: %w", crd.Name, err) } if len(list.Items) == 0 { return nil } for _, item := range list.Items { patches, err := ssa.PatchMigrateToVersion(&item, apiVersion) if err != nil { return fmt.Errorf("failed to create migration patch for %s/%s/%s: %w", item.GetKind(), item.GetNamespace(), item.GetName(), err) } if len(patches) == 0 { // patch the resource with an empty patch to update the version if err := c.kubeClient.Patch( ctx, &item, client.RawPatch(client.Merge.Type(), []byte("{}")), ); err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf(" %s/%s/%s failed to migrate: %w", item.GetKind(), item.GetNamespace(), item.GetName(), err) } } else { // patch the resource to migrate the managed fields to the latest apiVersion rawPatch, err := json.Marshal(patches) if err != nil { return fmt.Errorf("failed to marshal migration patch for %s/%s/%s: %w", item.GetKind(), item.GetNamespace(), item.GetName(), err) } if err := c.kubeClient.Patch( ctx, &item, client.RawPatch(types.JSONPatchType, rawPatch), ); err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf(" %s/%s/%s failed to migrate managed fields: %w", item.GetKind(), item.GetNamespace(), item.GetName(), err) } } logger.Successf("%s/%s/%s migrated to version %s", item.GetKind(), item.GetNamespace(), item.GetName(), version) } return nil } // getStorageVersion retrieves the storage version of a CustomResourceDefinition. func (c *ClusterMigrator) getStorageVersion(crd *apiextensionsv1.CustomResourceDefinition) string { var version string for _, v := range crd.Spec.Versions { if v.Storage { version = v.Name break } } return version } // WritableFS extends fs.FS with a WriteFile method. type WritableFS interface { fs.FS WriteFile(name string, data []byte, perm os.FileMode) error } // osFS is a WritableFS implementation that uses the file system of the OS. type osFS struct { fs.FS } func (o *osFS) WriteFile(name string, data []byte, perm os.FileMode) error { return os.WriteFile(name, data, perm) } // FileSystemMigrator migrates all the CRs found in the manifests located in the specified path. type FileSystemMigrator struct { fileSystem WritableFS yes bool dryRun bool path string extensions []string latestVersions map[schema.GroupKind]string } // FileAPIUpgrades represents the API upgrades detected in a specific manifest file. type FileAPIUpgrades struct { File string Upgrades []APIUpgrade } // APIUpgrade represents an upgrade of a specific API version in a manifest file. type APIUpgrade struct { Line int Kind string OldVersion string NewVersion string } // NewFileSystemMigrator creates a new FileSystemMigrator instance with the specified flags. func NewFileSystemMigrator(fileSystem WritableFS, yes, dryRun bool, path string, extensions []string, latestVersions map[schema.GroupKind]string) *FileSystemMigrator { return &FileSystemMigrator{ fileSystem: fileSystem, yes: yes, dryRun: dryRun, path: filepath.Clean(path), // convert dir/ to dir to avoid error when walking extensions: extensions, latestVersions: latestVersions, } } func (f *FileSystemMigrator) Run() error { logger.Actionf("starting migration of custom resources") // List and filter files. files, err := f.listFiles() if err != nil { return err } // Detect upgrades. upgrades, err := f.detectUpgrades(files) if err != nil { return err } if len(upgrades) == 0 { logger.Successf("no custom resources found that require migration") return nil } if f.dryRun { logger.Successf("dry-run mode enabled, no changes will be made") return nil } // Confirm upgrades. if !f.yes { prompt := promptui.Prompt{ Label: "Are you sure you want to proceed with the above upgrades", // Already prints "? [y/N]" IsConfirm: true, } if _, err := prompt.Run(); err != nil { return err } } // Migrate files. for _, fileUpgrades := range upgrades { if err := f.migrateFile(&fileUpgrades); err != nil { return err } logger.Successf("file %s migrated successfully", fileUpgrades.File) } logger.Successf("custom resources migrated successfully") return nil } func (f *FileSystemMigrator) listFiles() ([]string, error) { fileInfo, err := fs.Stat(f.fileSystem, f.path) if err != nil { return nil, fmt.Errorf("failed to stat path %s: %w", f.path, err) } if fileInfo.IsDir() { return f.listDirectoryFiles() } if err := f.validateSingleFile(); err != nil { return nil, err } return []string{f.path}, nil } func (f *FileSystemMigrator) listDirectoryFiles() ([]string, error) { var files []string err := fs.WalkDir(f.fileSystem, f.path, func(path string, dirEntry fs.DirEntry, err error) error { if err != nil { return err } if !f.matchesExtensions(path) { return nil } fileInfo, err := dirEntry.Info() if err != nil { return err } if fileInfo.Mode().IsRegular() { files = append(files, path) } else if !fileInfo.IsDir() { logger.Warningf("skipping irregular file %s", path) } return nil }) if err != nil { return nil, fmt.Errorf("failed to walk directory %s: %w", f.path, err) } return files, nil } func (f *FileSystemMigrator) validateSingleFile() error { if !f.matchesExtensions(f.path) { return fmt.Errorf("file %s does not match the specified extensions: %v", f.path, strings.Join(f.extensions, ", ")) } // Check if it's irregular by walking the parent directory. var irregular bool err := fs.WalkDir(f.fileSystem, filepath.Dir(f.path), func(path string, dirEntry fs.DirEntry, err error) error { if err != nil { return err } if path != f.path { return nil } fileInfo, err := dirEntry.Info() if err != nil { return err } if !fileInfo.Mode().IsRegular() { irregular = true } return nil }) if err != nil { return fmt.Errorf("failed to validate file %s: %w", f.path, err) } if irregular { return fmt.Errorf("file %s is irregular", f.path) } return nil } func (f *FileSystemMigrator) matchesExtensions(file string) bool { for _, ext := range f.extensions { if strings.HasSuffix(file, ext) { return true } } return false } func (f *FileSystemMigrator) detectUpgrades(files []string) ([]FileAPIUpgrades, error) { var upgrades []FileAPIUpgrades for _, file := range files { fileUpgrades, err := f.detectFileUpgrades(file) if err != nil { return nil, err } if len(fileUpgrades) == 0 { continue } fu := FileAPIUpgrades{ File: file, Upgrades: fileUpgrades, } upgrades = append(upgrades, fu) f.printDetectedUpgrades(&fu) } return upgrades, nil } func (f *FileSystemMigrator) detectFileUpgrades(file string) ([]APIUpgrade, error) { b, err := fs.ReadFile(f.fileSystem, file) if err != nil { return nil, fmt.Errorf("failed to read file %s: %w", file, err) } lines := strings.Split(string(b), "\n") var fileUpgrades []APIUpgrade for line, apiVersionLine := range lines { // Parse apiVersion. const apiVersionPrefix = "apiVersion: " idx := strings.Index(apiVersionLine, apiVersionPrefix) if idx == -1 { continue } apiVersionValuePrefix := strings.TrimSpace(apiVersionLine[idx+len(apiVersionPrefix):]) apiVersion := strings.Split(apiVersionValuePrefix, " ")[0] gv, err := schema.ParseGroupVersion(apiVersion) if err != nil { logger.Warningf("%s:%d: %v", file, line+1, err) continue } // Parse kind. if line+1 >= len(lines) { continue } kindLine := lines[line+1] const kindPrefix = "kind: " idx = strings.Index(kindLine, kindPrefix) if idx == -1 { continue } kindValuePrefix := strings.TrimSpace(kindLine[idx+len(kindPrefix):]) kind := strings.Split(kindValuePrefix, " ")[0] // Build GroupKind. gk := schema.GroupKind{ Group: gv.Group, Kind: kind, } // Check if there's a newer version for the GroupKind. latestVersion, ok := f.latestVersions[gk] if !ok || latestVersion == gv.Version { continue } // Record the upgrade. fileUpgrades = append(fileUpgrades, APIUpgrade{ Line: line, Kind: kind, OldVersion: gv.Version, NewVersion: latestVersion, }) } return fileUpgrades, nil } func (f *FileSystemMigrator) printDetectedUpgrades(fileUpgrades *FileAPIUpgrades) { for _, upgrade := range fileUpgrades.Upgrades { logger.Generatef("%s:%d: %s %s -> %s", fileUpgrades.File, upgrade.Line+1, upgrade.Kind, upgrade.OldVersion, upgrade.NewVersion) } } func (f *FileSystemMigrator) migrateFile(fileUpgrades *FileAPIUpgrades) error { // Read file and map lines. b, err := fs.ReadFile(f.fileSystem, fileUpgrades.File) if err != nil { return fmt.Errorf("failed to read file %s: %w", fileUpgrades.File, err) } lines := strings.Split(string(b), "\n") // Apply upgrades to lines. for _, upgrade := range fileUpgrades.Upgrades { line := lines[upgrade.Line] line = strings.Replace(line, upgrade.OldVersion, upgrade.NewVersion, 1) lines[upgrade.Line] = line } // Read file info to preserve permissions. fileInfo, err := fs.Stat(f.fileSystem, fileUpgrades.File) if err != nil { return fmt.Errorf("failed to stat file %s: %w", fileUpgrades.File, err) } // Write file with preserved permissions. b = []byte(strings.Join(lines, "\n")) if err := f.fileSystem.WriteFile(fileUpgrades.File, b, fileInfo.Mode()); err != nil { return fmt.Errorf("failed to write file %s: %w", fileUpgrades.File, err) } return nil } ================================================ FILE: cmd/flux/migrate_test.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "io/fs" "os" "testing" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/runtime/schema" ) type writeToMemoryFS struct { fs.FS writtenFiles map[string][]byte } func (m *writeToMemoryFS) WriteFile(name string, data []byte, perm os.FileMode) error { m.writtenFiles[name] = data return nil } type writtenFile struct { file string goldenFile string } func TestFileSystemMigrator(t *testing.T) { for _, tt := range []struct { name string path string outputGolden string writtenFiles []writtenFile err string }{ { name: "errors out for single file that is a symlink", path: "testdata/migrate/file-system/single-file-link.yaml", err: "file testdata/migrate/file-system/single-file-link.yaml is irregular", }, { name: "errors out for single file with wrong extension", path: "testdata/migrate/file-system/single-file-wrong-ext.json", err: "file testdata/migrate/file-system/single-file-wrong-ext.json does not match the specified extensions: .yaml, .yml", }, { name: "migrate single file", path: "testdata/migrate/file-system/single-file.yaml", outputGolden: "testdata/migrate/file-system/single-file.yaml.output.golden", writtenFiles: []writtenFile{ { file: "testdata/migrate/file-system/single-file.yaml", goldenFile: "testdata/migrate/file-system/single-file.yaml.golden", }, }, }, { name: "migrate files in directory", path: "testdata/migrate/file-system/dir", outputGolden: "testdata/migrate/file-system/dir.output.golden", writtenFiles: []writtenFile{ { file: "testdata/migrate/file-system/dir/some-dir/another-file.yaml", goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yaml", }, { file: "testdata/migrate/file-system/dir/some-dir/another-file.yml", goldenFile: "testdata/migrate/file-system/dir.golden/some-dir/another-file.yml", }, { file: "testdata/migrate/file-system/dir/some-file.yaml", goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yaml", }, { file: "testdata/migrate/file-system/dir/some-file.yml", goldenFile: "testdata/migrate/file-system/dir.golden/some-file.yml", }, }, }, } { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) // Store logger, replace with test logger, and restore at the end of the test. var testLogger bytes.Buffer oldLogger := logger logger = stderrLogger{&testLogger} t.Cleanup(func() { logger = oldLogger }) // Open current working directory as root and build write-to-memory filesystem. pathRoot, err := os.OpenRoot(".") g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(func() { pathRoot.Close() }) fileSystem := &writeToMemoryFS{ FS: pathRoot.FS(), writtenFiles: make(map[string][]byte), } // Prepare other inputs. const yes = true const dryRun = false extensions := []string{".yaml", ".yml"} latestVersions := map[schema.GroupKind]string{ {Group: "image.toolkit.fluxcd.io", Kind: "ImageRepository"}: "v1", {Group: "image.toolkit.fluxcd.io", Kind: "ImagePolicy"}: "v1", {Group: "image.toolkit.fluxcd.io", Kind: "ImageUpdateAutomation"}: "v1", } // Run migration. err = NewFileSystemMigrator(fileSystem, yes, dryRun, tt.path, extensions, latestVersions).Run() if tt.err != "" { g.Expect(err).To(HaveOccurred()) g.Expect(err.Error()).To(Equal(tt.err)) return } g.Expect(err).ToNot(HaveOccurred()) // Assert logger output. b, err := os.ReadFile(tt.outputGolden) g.Expect(err).ToNot(HaveOccurred()) g.Expect(string(b)).To(Equal(testLogger.String()), "logger output does not match golden file %s", tt.outputGolden) // Assert which files were written. writtenFiles := make([]string, 0, len(fileSystem.writtenFiles)) for name := range fileSystem.writtenFiles { writtenFiles = append(writtenFiles, name) } expectedWrittenFiles := make([]string, 0, len(tt.writtenFiles)) for _, wf := range tt.writtenFiles { expectedWrittenFiles = append(expectedWrittenFiles, wf.file) } g.Expect(writtenFiles).To(ConsistOf(expectedWrittenFiles)) // Assert contents of written files. for _, wf := range tt.writtenFiles { b, err := os.ReadFile(wf.goldenFile) g.Expect(err).ToNot(HaveOccurred()) g.Expect(string(fileSystem.writtenFiles[wf.file])).To(Equal(string(b)), "file %s does not match golden file %s", wf.file, wf.goldenFile) } }) } } ================================================ FILE: cmd/flux/object.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" ) // Most commands need one or both of the kind (e.g., // `"ImageRepository"`) and a human-palatable name for the kind (e.g., // `"image repository"`), to be interpolated into output. It's // convenient to package these up ahead of time, then the command // implementation can pick whichever it wants to use. type apiType struct { kind, humanKind string groupVersion schema.GroupVersion } // adapter is an interface for a wrapper or alias from which we can // get a controller-runtime deserialisable value. This is used so that // you can wrap an API type to give it other useful methods, but still // use values of the wrapper with `client.Client`, which only deals // with types that have been added to the schema. type adapter interface { asClientObject() client.Object } // copyable is an interface for a wrapper or alias from which we can // get a deep copied client.Object, required when you e.g. want to // calculate a patch. type copyable interface { deepCopyClientObject() client.Object } // listAdapter is the analogue to adapter, but for lists; the // controller runtime distinguishes between methods dealing with // objects and lists. type listAdapter interface { asClientList() client.ObjectList len() int } // universalAdapter is an adapter for any client.Object. Use this if // there are no other methods needed. type universalAdapter struct { obj client.Object } func (c universalAdapter) asClientObject() client.Object { return c.obj } // named is for adapters that have Name and Namespace fields, which // are sometimes handy to get hold of. ObjectMeta implements these, so // they shouldn't need any extra work. type named interface { GetName() string GetNamespace() string GetObjectKind() schema.ObjectKind SetName(string) SetNamespace(string) } func copyName(target, source named) { target.SetName(source.GetName()) target.SetNamespace(source.GetNamespace()) } ================================================ FILE: cmd/flux/oci.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" "github.com/fluxcd/pkg/auth" "github.com/fluxcd/pkg/auth/azure" authutils "github.com/fluxcd/pkg/auth/utils" ) // loginWithProvider gets a crane authentication option for the given provider and URL. func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, authn.Authenticator, error) { var opts []auth.Option if provider == azure.ProviderName { opts = append(opts, auth.WithAllowShellOut()) } authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...) if err != nil { return nil, nil, fmt.Errorf("could not login to provider %s with url %s: %w", provider, url, err) } return crane.WithAuth(authenticator), authenticator, nil } ================================================ FILE: cmd/flux/pull.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var pullCmd = &cobra.Command{ Use: "pull", Short: "Pull artifacts", Long: `The pull command is used to download OCI artifacts.`, } func init() { rootCmd.AddCommand(pullCmd) } ================================================ FILE: cmd/flux/pull_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "os" "github.com/google/go-containerregistry/pkg/crane" "github.com/spf13/cobra" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" ) var pullArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Pull artifact", Long: `The pull artifact command downloads and extracts the OCI artifact content to the given path. The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`, Example: ` # Pull an OCI artifact created by flux from GHCR flux pull artifact oci://ghcr.io/org/manifests/app:v0.0.1 --output ./path/to/local/manifests `, RunE: pullArtifactCmdRun, } type pullArtifactFlags struct { output string creds string insecure bool provider flags.SourceOCIProvider } var pullArtifactArgs = newPullArtifactFlags() func newPullArtifactFlags() pullArtifactFlags { return pullArtifactFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } func init() { pullArtifactCmd.Flags().StringVarP(&pullArtifactArgs.output, "output", "o", "", "path where the artifact content should be extracted.") pullArtifactCmd.Flags().StringVar(&pullArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") pullArtifactCmd.Flags().Var(&pullArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) pullArtifactCmd.Flags().BoolVar(&pullArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pulled without TLS") pullCmd.AddCommand(pullArtifactCmd) } func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact URL is required") } ociURL := args[0] if pullArtifactArgs.output == "" { return fmt.Errorf("output path cannot be empty") } if fs, err := os.Stat(pullArtifactArgs.output); err != nil || !fs.IsDir() { return fmt.Errorf("invalid output path %q: %w", pullArtifactArgs.output, err) } url, err := oci.ParseArtifactURL(ociURL) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() opts := oci.DefaultOptions() if pullArtifactArgs.insecure { opts = append(opts, crane.Insecure) } if pullArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { logger.Actionf("logging in to registry with provider credentials") opt, _, err := loginWithProvider(ctx, url, pullArtifactArgs.provider.String()) if err != nil { return fmt.Errorf("error during login with provider: %w", err) } opts = append(opts, opt) } ociClient := oci.NewClient(opts) if pullArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && pullArtifactArgs.creds != "" { logger.Actionf("logging in to registry with credentials") if err := ociClient.LoginWithCredentials(pullArtifactArgs.creds); err != nil { return fmt.Errorf("could not login with credentials: %w", err) } } logger.Actionf("pulling artifact from %s", url) meta, err := ociClient.Pull(ctx, url, pullArtifactArgs.output) if err != nil { return err } if meta.Source != "" { logger.Successf("source %s", meta.Source) } if meta.Revision != "" { logger.Successf("revision %s", meta.Revision) } logger.Successf("digest %s", meta.Digest) logger.Successf("artifact content extracted to %s", pullArtifactArgs.output) return nil } ================================================ FILE: cmd/flux/push.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var pushCmd = &cobra.Command{ Use: "push", Short: "Push artifacts", Long: `The push command is used to publish OCI artifacts.`, } func init() { rootCmd.AddCommand(pushCmd) } ================================================ FILE: cmd/flux/push_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "fmt" "os" "strings" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/logs" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/spf13/cobra" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" ) var pushArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Push artifact", Long: `The push artifact command creates a tarball from the given directory or the single file and uploads the artifact to an OCI repository. The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`, Example: ` # Push manifests to GHCR using the short Git SHA as the OCI artifact tag echo $GITHUB_PAT | docker login ghcr.io --username flux --password-stdin flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ --path="./path/to/local/manifests" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" # Push and sign artifact with cosign digest_url = $(flux push artifact \ oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ --source="$(git config --get remote.origin.url)" \ --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" \ --path="./path/to/local/manifest.yaml" \ --output json | \ jq -r '. | .repository + "@" + .digest') cosign sign $digest_url # Push manifests passed into stdin to GHCR and set custom OCI annotations kustomize build . | flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) -f - \ --source="$(git config --get remote.origin.url)" \ --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" \ --annotations='org.opencontainers.image.licenses=Apache-2.0' \ --annotations='org.opencontainers.image.documentation=https://app.org/docs' \ --annotations='org.opencontainers.image.description=Production config.' # Push single manifest file to GHCR using the short Git SHA as the OCI artifact tag echo $GITHUB_PAT | docker login ghcr.io --username flux --password-stdin flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ --path="./path/to/local/manifest.yaml" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" # Push manifests to Docker Hub using the Git tag as the OCI artifact tag echo $DOCKER_PAT | docker login --username flux --password-stdin flux push artifact oci://docker.io/org/app-config:$(git tag --points-at HEAD) \ --path="./path/to/local/manifests" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" # Login directly to the registry provider # You might need to export the following variable if you use local config files for AWS: # export AWS_SDK_LOAD_CONFIG=1 flux push artifact oci://.dkr.ecr..amazonaws.com/app-config:$(git tag --points-at HEAD) \ --path="./path/to/local/manifests" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" \ --provider aws # Login by passing credentials directly flux push artifact oci://docker.io/org/app-config:$(git tag --points-at HEAD) \ --path="./path/to/local/manifests" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" \ --creds flux:$DOCKER_PAT `, RunE: pushArtifactCmdRun, } type pushArtifactFlags struct { path string source string revision string creds string provider flags.SourceOCIProvider ignorePaths []string annotations []string output string debug bool reproducible bool insecure bool } var pushArtifactArgs = newPushArtifactFlags() func newPushArtifactFlags() pushArtifactFlags { return pushArtifactFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } func init() { pushArtifactCmd.Flags().StringVarP(&pushArtifactArgs.path, "path", "f", "", "path to the directory where the Kubernetes manifests are located") pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.source, "source", "", "the source address, e.g. the Git URL") pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.revision, "revision", "", "the source revision in the format '@sha1:'") pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") pushArtifactCmd.Flags().Var(&pushArtifactArgs.provider, "provider", pushArtifactArgs.provider.Description()) pushArtifactCmd.Flags().StringSliceVar(&pushArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") pushArtifactCmd.Flags().StringArrayVarP(&pushArtifactArgs.annotations, "annotations", "a", nil, "Set custom OCI annotations in the format '='") pushArtifactCmd.Flags().StringVarP(&pushArtifactArgs.output, "output", "o", "", "the format in which the artifact digest should be printed, can be 'json' or 'yaml'") pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS") pushCmd.AddCommand(pushArtifactCmd) } func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact URL is required") } ociURL := args[0] if pushArtifactArgs.source == "" { return fmt.Errorf("--source is required") } if pushArtifactArgs.revision == "" { return fmt.Errorf("--revision is required") } if pushArtifactArgs.path == "" { return fmt.Errorf("invalid path %q", pushArtifactArgs.path) } url, err := oci.ParseArtifactURL(ociURL) if err != nil { return err } ref, err := name.ParseReference(url) if err != nil { return err } path := pushArtifactArgs.path if pushArtifactArgs.path == "-" { path, err = saveReaderToFile(os.Stdin) if err != nil { return err } defer os.Remove(path) } if _, err := os.Stat(path); err != nil { return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err) } annotations := map[string]string{} for _, annotation := range pushArtifactArgs.annotations { kv := strings.Split(annotation, "=") if len(kv) != 2 { return fmt.Errorf("invalid annotation %s, must be in the format key=value", annotation) } annotations[kv[0]] = kv[1] } if pushArtifactArgs.debug { // direct logs from crane library to stderr // this can be useful to figure out things happening underneath e.g when the library is retrying a request logs.Warn.SetOutput(os.Stderr) } meta := oci.Metadata{ Source: pushArtifactArgs.source, Revision: pushArtifactArgs.revision, Annotations: annotations, } if pushArtifactArgs.reproducible { zeroTime := time.Unix(0, 0) meta.Created = zeroTime.Format(time.RFC3339) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() var authenticator authn.Authenticator opts := oci.DefaultOptions() if pushArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && pushArtifactArgs.creds != "" { logger.Actionf("logging in to registry with credentials") authenticator, err = oci.GetAuthFromCredentials(pushArtifactArgs.creds) if err != nil { return fmt.Errorf("could not login with credentials: %w", err) } opts = append(opts, crane.WithAuth(authenticator)) } if provider := pushArtifactArgs.provider.String(); provider != sourcev1.GenericOCIProvider { logger.Actionf("logging in to registry with provider credentials") var opt crane.Option opt, authenticator, err = loginWithProvider(ctx, url, provider) if err != nil { return fmt.Errorf("error during login with provider: %w", err) } opts = append(opts, opt) } if rootArgs.timeout != 0 { backoff := remote.Backoff{ Duration: 1.0 * time.Second, Factor: 3, Jitter: 0.1, // timeout happens when the cap is exceeded or number of steps is reached // 10 steps is big enough that most reasonable cap(under 30min) will be exceeded before // the number of steps are completed. Steps: 10, Cap: rootArgs.timeout, } if authenticator == nil { authenticator, err = authn.DefaultKeychain.Resolve(ref.Context()) if err != nil { return err } } transportOpts, err := oci.WithRetryTransport(ctx, ref, authenticator, backoff, []string{ref.Context().Scope(transport.PushScope)}, pushArtifactArgs.insecure, ) if err != nil { return fmt.Errorf("error setting up transport: %w", err) } opts = append(opts, transportOpts, oci.WithRetryBackOff(backoff)) } if pushArtifactArgs.output == "" { logger.Actionf("pushing artifact to %s", url) } if pushArtifactArgs.insecure { opts = append(opts, crane.Insecure) } ociClient := oci.NewClient(opts) digestURL, err := ociClient.Push(ctx, url, path, oci.WithPushMetadata(meta), oci.WithPushIgnorePaths(pushArtifactArgs.ignorePaths...), ) if err != nil { return fmt.Errorf("pushing artifact failed: %w", err) } digest, err := name.NewDigest(digestURL) if err != nil { return fmt.Errorf("artifact digest parsing failed: %w", err) } tag, err := name.NewTag(url) if err != nil { return fmt.Errorf("artifact tag parsing failed: %w", err) } info := struct { URL string `json:"url"` Repository string `json:"repository"` Tag string `json:"tag"` Digest string `json:"digest"` }{ URL: fmt.Sprintf("oci://%s", digestURL), Repository: digest.Repository.Name(), Tag: tag.TagStr(), Digest: digest.DigestStr(), } switch pushArtifactArgs.output { case "json": marshalled, err := json.MarshalIndent(&info, "", " ") if err != nil { return fmt.Errorf("artifact digest JSON conversion failed: %w", err) } marshalled = append(marshalled, "\n"...) rootCmd.Print(string(marshalled)) case "yaml": marshalled, err := yaml.Marshal(&info) if err != nil { return fmt.Errorf("artifact digest YAML conversion failed: %w", err) } rootCmd.Print(string(marshalled)) default: logger.Successf("artifact successfully pushed to %s", digestURL) } return nil } ================================================ FILE: cmd/flux/readiness.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/object" "github.com/fluxcd/pkg/runtime/patch" ) // objectStatusType is the type of object in terms of status when computing the // readiness of an object. Readiness check method depends on the type of object. // For a dynamic object, Ready status condition is considered only for the // latest generation of the object. For a static object that don't have any // condition, the object generation is not considered. type objectStatusType int const ( objectStatusDynamic objectStatusType = iota objectStatusStatic ) // isObjectReady determines if an object is ready using the kstatus.Compute() // result. statusType helps differenciate between static and dynamic objects to // accurately check the object's readiness. A dynamic object may have some extra // considerations depending on the object. func isObjectReady(obj client.Object, statusType objectStatusType) (bool, error) { observedGen, err := object.GetStatusObservedGeneration(obj) if err != nil && err != object.ErrObservedGenerationNotFound { return false, err } if statusType == objectStatusDynamic { // Object not reconciled yet. if observedGen < 1 { return false, nil } cobj, ok := obj.(meta.ObjectWithConditions) if !ok { return false, fmt.Errorf("unable to get conditions from object") } if c := apimeta.FindStatusCondition(cobj.GetConditions(), meta.ReadyCondition); c != nil { // Ensure that the ready condition is for the latest generation of // the object. // NOTE: Some APIs like ImageUpdateAutomation and HelmRelease don't // support per condition observed generation yet. Per condition // observed generation for them are always zero. // There are two strategies used across different object kinds to // check the latest ready condition: // - check that the ready condition's generation matches the // object's generation. // - check that the observed generation of the object in the // status matches the object's generation. // // TODO: Once ImageUpdateAutomation and HelmRelease APIs have per // condition observed generation, remove the object's observed // generation and object's generation check (the second condition // below). Also, try replacing this readiness check function with // fluxcd/pkg/ssa's ResourceManager.Wait(), which uses kstatus // internally to check readiness of the objects. if c.ObservedGeneration != 0 && c.ObservedGeneration != obj.GetGeneration() { return false, nil } if c.ObservedGeneration == 0 && observedGen != obj.GetGeneration() { return false, nil } } else { return false, nil } } u, err := patch.ToUnstructured(obj) if err != nil { return false, err } result, err := kstatus.Compute(u) if err != nil { return false, err } switch result.Status { case kstatus.CurrentStatus: return true, nil case kstatus.InProgressStatus: return false, nil default: return false, fmt.Errorf("%s", result.Message) } } // isObjectReadyConditionFunc returns a wait.ConditionFunc to be used with // wait.Poll* while polling for an object with dynamic status to be ready. func isObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { return func(ctx context.Context) (bool, error) { err := kubeClient.Get(ctx, namespaceName, obj) if err != nil { return false, err } return isObjectReady(obj, objectStatusDynamic) } } // isStaticObjectReadyConditionFunc returns a wait.ConditionFunc to be used with // wait.Poll* while polling for an object with static or no status to be // ready. func isStaticObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { return func(ctx context.Context) (bool, error) { err := kubeClient.Get(ctx, namespaceName, obj) if err != nil { return false, err } return isObjectReady(obj, objectStatusStatic) } } // kstatusCompute returns the kstatus computed result of a given object. func kstatusCompute(obj client.Object) (result *kstatus.Result, err error) { u, err := patch.ToUnstructured(obj) if err != nil { return result, err } return kstatus.Compute(u) } ================================================ FILE: cmd/flux/readiness_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func Test_isObjectReady(t *testing.T) { // Ready object. readyObj := &sourcev1.GitRepository{} readyObj.Generation = 1 readyObj.Status.ObservedGeneration = 1 conditions.MarkTrue(readyObj, meta.ReadyCondition, "foo1", "bar1") // Not ready object. notReadyObj := readyObj.DeepCopy() conditions.MarkFalse(notReadyObj, meta.ReadyCondition, "foo2", "bar2") // Not reconciled object. notReconciledObj := readyObj.DeepCopy() notReconciledObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: -1} // No condition. noConditionObj := readyObj.DeepCopy() noConditionObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: 1} // Outdated condition. readyObjOutdated := readyObj.DeepCopy() readyObjOutdated.Generation = 2 // Object without per condition observed generation. oldObj := readyObj.DeepCopy() readyTrueCondn := conditions.TrueCondition(meta.ReadyCondition, "foo3", "bar3") oldObj.Status.Conditions = []metav1.Condition{*readyTrueCondn} // Outdated object without per condition observed generation. oldObjOutdated := oldObj.DeepCopy() oldObjOutdated.Generation = 2 // Empty status object. staticObj := readyObj.DeepCopy() staticObj.Status = sourcev1.GitRepositoryStatus{} // No status object. noStatusObj := ¬ificationv1.Provider{} noStatusObj.Generation = 1 type args struct { obj client.Object statusType objectStatusType } tests := []struct { name string args args want bool wantErr bool }{ { name: "dynamic ready", args: args{obj: readyObj, statusType: objectStatusDynamic}, want: true, }, { name: "dynamic not ready", args: args{obj: notReadyObj, statusType: objectStatusDynamic}, want: false, }, { name: "dynamic not reconciled", args: args{obj: notReconciledObj, statusType: objectStatusDynamic}, want: false, }, { name: "dynamic not condition", args: args{obj: noConditionObj, statusType: objectStatusDynamic}, want: false, }, { name: "dynamic ready outdated", args: args{obj: readyObjOutdated, statusType: objectStatusDynamic}, want: false, }, { name: "dynamic ready without per condition gen", args: args{obj: oldObj, statusType: objectStatusDynamic}, want: true, }, { name: "dynamic outdated ready status without per condition gen", args: args{obj: oldObjOutdated, statusType: objectStatusDynamic}, want: false, }, { name: "static empty status", args: args{obj: staticObj, statusType: objectStatusStatic}, want: true, }, { name: "static no status", args: args{obj: noStatusObj, statusType: objectStatusStatic}, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := isObjectReady(tt.args.obj, tt.args.statusType) if (err != nil) != tt.wantErr { t.Errorf("isObjectReady() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("isObjectReady() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: cmd/flux/receiver.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) // notificationv1.Receiver var receiverType = apiType{ kind: notificationv1.ReceiverKind, humanKind: "receiver", groupVersion: notificationv1.GroupVersion, } type receiverAdapter struct { *notificationv1.Receiver } func (a receiverAdapter) asClientObject() client.Object { return a.Receiver } func (a receiverAdapter) deepCopyClientObject() client.Object { return a.Receiver.DeepCopy() } // notificationv1.Receiver type receiverListAdapter struct { *notificationv1.ReceiverList } func (a receiverListAdapter) asClientList() client.ObjectList { return a.ReceiverList } func (a receiverListAdapter) len() int { return len(a.ReceiverList.Items) } ================================================ FILE: cmd/flux/reconcile.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "time" kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" ) var reconcileCmd = &cobra.Command{ Use: "reconcile", Short: "Reconcile sources and resources", Long: `The reconcile sub-commands trigger a reconciliation of sources and resources.`, } func init() { rootCmd.AddCommand(reconcileCmd) } type reconcileCommand struct { apiType object reconcilable } type reconcilable interface { adapter // to be able to load from the cluster copyable // to be able to calculate patches suspendable // to tell if it's suspended // these are implemented by anything embedding metav1.ObjectMeta GetAnnotations() map[string]string SetAnnotations(map[string]string) isStatic() bool // is it a static object that does not have a reconciler? lastHandledReconcileRequest() string // what was the last handled reconcile request? successMessage() string // what do you want to tell people when successfully reconciled? } func reconcilableConditions(object reconcilable) []metav1.Condition { if s, ok := object.(meta.ObjectWithConditions); ok { return s.GetConditions() } if s, ok := object.(oldConditions); ok { return *s.GetStatusConditions() } return []metav1.Condition{} } func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("%s name is required", reconcile.kind) } name := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, reconcile.object.asClientObject()) if err != nil { return err } if reconcile.object.isStatic() { logger.Successf("reconciliation not supported by the object") return nil } if reconcile.object.isSuspended() { return fmt.Errorf("resource is suspended") } logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.groupVersion.WithKind(reconcile.kind)); err != nil { return err } logger.Successf("%s annotated", reconcile.kind) lastHandledReconcileAt := reconcile.object.lastHandledReconcileRequest() logger.Waitingf("waiting for %s reconciliation", reconcile.kind) if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, reconciliationHandled(kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { return err } readyCond := apimeta.FindStatusCondition(reconcilableConditions(reconcile.object), meta.ReadyCondition) if readyCond == nil { return fmt.Errorf("status can't be determined") } if readyCond.Status != metav1.ConditionTrue { return fmt.Errorf("%s reconciliation failed: '%s'", reconcile.kind, readyCond.Message) } logger.Successf("%s", reconcile.object.successMessage()) return nil } func reconciliationHandled(kubeClient client.Client, namespacedName types.NamespacedName, obj reconcilable, lastHandledReconcileAt string) wait.ConditionWithContextFunc { return func(ctx context.Context) (bool, error) { err := kubeClient.Get(ctx, namespacedName, obj.asClientObject()) if err != nil { return false, err } if obj.lastHandledReconcileRequest() == lastHandledReconcileAt { return false, nil } result, err := kstatusCompute(obj.asClientObject()) if err != nil { return false, err } switch result.Status { case kstatus.CurrentStatus: return true, nil case kstatus.InProgressStatus: return false, nil default: return false, fmt.Errorf("%s", result.Message) } } } func requestReconciliation(ctx context.Context, kubeClient client.Client, namespacedName types.NamespacedName, gvk schema.GroupVersionKind) error { return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { object := &metav1.PartialObjectMetadata{} object.SetGroupVersionKind(gvk) object.SetName(namespacedName.Name) object.SetNamespace(namespacedName.Namespace) if err := kubeClient.Get(ctx, namespacedName, object); err != nil { return err } patch := client.MergeFrom(object.DeepCopy()) // Add a timestamp annotation to trigger a reconciliation. ts := time.Now().Format(time.RFC3339Nano) annotations := object.GetAnnotations() if annotations == nil { annotations = make(map[string]string, 1) } annotations[meta.ReconcileRequestAnnotation] = ts // HelmRelease specific annotations to force or reset a release. if gvk.Kind == helmv2.HelmReleaseKind { if rhrArgs.syncForce { annotations[helmv2.ForceRequestAnnotation] = ts } if rhrArgs.syncReset { annotations[helmv2.ResetRequestAnnotation] = ts } } object.SetAnnotations(annotations) return kubeClient.Patch(ctx, object, patch) }) } ================================================ FILE: cmd/flux/reconcile_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileHrCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Reconcile a HelmRelease resource", Long: ` The reconcile helmrelease command triggers a reconciliation of a HelmRelease resource and waits for it to finish.`, Example: ` # Trigger a HelmRelease apply outside of the reconciliation interval flux reconcile hr podinfo # Trigger a reconciliation of the HelmRelease's source and apply changes flux reconcile hr podinfo --with-source`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: reconcileWithSourceCommand{ apiType: helmReleaseType, object: helmReleaseAdapter{&helmv2.HelmRelease{}}, }.run, } type reconcileHelmReleaseFlags struct { syncHrWithSource bool syncForce bool syncReset bool } var rhrArgs reconcileHelmReleaseFlags func init() { reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncHrWithSource, "with-source", false, "reconcile HelmRelease source") reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncForce, "force", false, "force a one-off install or upgrade of the HelmRelease resource") reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncReset, "reset", false, "reset the failure count for this HelmRelease resource") reconcileCmd.AddCommand(reconcileHrCmd) } func (obj helmReleaseAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj helmReleaseAdapter) reconcileSource() bool { return rhrArgs.syncHrWithSource } func (obj helmReleaseAdapter) getSource() (reconcileSource, sourceReference) { var ( name string ns string ) switch { case obj.Spec.ChartRef != nil: name, ns = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace if ns == "" { ns = obj.Namespace } srcRef := sourceReference{ kind: obj.Spec.ChartRef.Kind, name: name, namespace: ns, } switch obj.Spec.ChartRef.Kind { case sourcev1.HelmChartKind: return reconcileWithSourceCommand{ apiType: helmChartType, object: helmChartAdapter{&sourcev1.HelmChart{}}, force: true, }, srcRef case sourcev1.OCIRepositoryKind: return reconcileCommand{ apiType: ociRepositoryType, object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, }, srcRef default: return nil, srcRef } default: // default case assumes the HelmRelease is using a HelmChartTemplate ns = obj.Spec.Chart.Spec.SourceRef.Namespace if ns == "" { ns = obj.Namespace } name = fmt.Sprintf("%s-%s", obj.Namespace, obj.Name) return reconcileWithSourceCommand{ apiType: helmChartType, object: helmChartAdapter{&sourcev1.HelmChart{}}, force: true, }, sourceReference{ kind: sourcev1.HelmChartKind, name: name, namespace: ns, } } } func (obj helmReleaseAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var reconcileImageCmd = &cobra.Command{ Use: "image", Short: "Reconcile image automation objects", Long: `The reconcile sub-commands trigger a reconciliation of image automation objects.`, } func init() { reconcileCmd.AddCommand(reconcileImageCmd) } ================================================ FILE: cmd/flux/reconcile_image_policy.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" "github.com/spf13/cobra" ) var reconcileImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Reconcile an ImagePolicy", Long: `The reconcile image policy command triggers a reconciliation of an ImagePolicy resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing image policy called 'alpine' flux reconcile image policy alpine`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: reconcileCommand{ apiType: imagePolicyType, object: imagePolicyAdapter{&imagev1.ImagePolicy{}}, }.run, } func init() { reconcileImageCmd.AddCommand(reconcileImagePolicyCmd) } ================================================ FILE: cmd/flux/reconcile_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var reconcileImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Reconcile an ImageRepository", Long: `The reconcile image repository command triggers a reconciliation of an ImageRepository resource and waits for it to finish.`, Example: ` # Trigger an scan for an existing image repository flux reconcile image repository alpine`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: reconcileCommand{ apiType: imageRepositoryType, object: imageRepositoryAdapter{&imagev1.ImageRepository{}}, }.run, } func init() { reconcileImageCmd.AddCommand(reconcileImageRepositoryCmd) } func (obj imageRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj imageRepositoryAdapter) successMessage() string { return fmt.Sprintf("scan fetched %d tags", obj.Status.LastScanResult.TagCount) } func (obj imageRepositoryAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_image_updateauto.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "time" "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" autov1 "github.com/fluxcd/image-automation-controller/api/v1" meta "github.com/fluxcd/pkg/apis/meta" ) var reconcileImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Reconcile an ImageUpdateAutomation", Long: `The reconcile image update command triggers a reconciliation of an ImageUpdateAutomation resource and waits for it to finish.`, Example: ` # Trigger an automation run for an existing image update automation flux reconcile image update latest-images`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: reconcileCommand{ apiType: imageUpdateAutomationType, object: imageUpdateAutomationAdapter{&autov1.ImageUpdateAutomation{}}, }.run, } func init() { reconcileImageCmd.AddCommand(reconcileImageUpdateCmd) } func (obj imageUpdateAutomationAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj imageUpdateAutomationAdapter) successMessage() string { if rc := apimeta.FindStatusCondition(obj.Status.Conditions, meta.ReadyCondition); rc != nil { return rc.Message } if obj.Status.LastAutomationRunTime != nil { return "last run " + obj.Status.LastAutomationRunTime.Time.Format(time.RFC3339) } return "automation not yet run" } func (obj imageUpdateAutomationAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Reconcile a Kustomization resource", Long: ` The reconcile kustomization command triggers a reconciliation of a Kustomization resource and waits for it to finish.`, Example: ` # Trigger a Kustomization apply outside of the reconciliation interval flux reconcile kustomization podinfo # Trigger a sync of the Kustomization's source and apply changes flux reconcile kustomization podinfo --with-source`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: reconcileWithSourceCommand{ apiType: kustomizationType, object: kustomizationAdapter{&kustomizev1.Kustomization{}}, }.run, } type reconcileKsFlags struct { syncKsWithSource bool } var rksArgs reconcileKsFlags func init() { reconcileKsCmd.Flags().BoolVar(&rksArgs.syncKsWithSource, "with-source", false, "reconcile Kustomization source") reconcileCmd.AddCommand(reconcileKsCmd) } func (obj kustomizationAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj kustomizationAdapter) reconcileSource() bool { return rksArgs.syncKsWithSource } func (obj kustomizationAdapter) getSource() (reconcileSource, sourceReference) { var cmd reconcileSource switch obj.Spec.SourceRef.Kind { case sourcev1.OCIRepositoryKind: cmd = reconcileCommand{ apiType: ociRepositoryType, object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, } case sourcev1.GitRepositoryKind: cmd = reconcileCommand{ apiType: gitRepositoryType, object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, } case sourcev1.BucketKind: cmd = reconcileCommand{ apiType: bucketType, object: bucketAdapter{&sourcev1.Bucket{}}, } } return cmd, sourceReference{ kind: obj.Spec.SourceRef.Kind, name: obj.Spec.SourceRef.Name, namespace: obj.Spec.SourceRef.Namespace, } } func (obj kustomizationAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var reconcileReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Reconcile a Receiver", Long: `The reconcile receiver command triggers a reconciliation of a Receiver resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing receiver flux reconcile receiver main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: reconcileCommand{ apiType: receiverType, object: receiverAdapter{¬ificationv1.Receiver{}}, }.run, } func init() { reconcileCmd.AddCommand(reconcileReceiverCmd) } func (obj receiverAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj receiverAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var reconcileSourceCmd = &cobra.Command{ Use: "source", Short: "Reconcile sources", Long: `The reconcile source sub-commands trigger a reconciliation of sources.`, } func init() { reconcileCmd.AddCommand(reconcileSourceCmd) } ================================================ FILE: cmd/flux/reconcile_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Reconcile a Bucket source", Long: `The reconcile source command triggers a reconciliation of a Bucket resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing source flux reconcile source bucket podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: reconcileCommand{ apiType: bucketType, object: bucketAdapter{&sourcev1.Bucket{}}, }.run, } func init() { reconcileSourceCmd.AddCommand(reconcileSourceBucketCmd) } func (obj bucketAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj bucketAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } func (obj bucketAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_source_chart.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceHelmChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Reconcile a HelmChart source", Long: `The reconcile source command triggers a reconciliation of a HelmChart resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing source flux reconcile source chart podinfo # Trigger a reconciliation of the HelmCharts's source and apply changes flux reconcile helmchart podinfo --with-source`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: reconcileWithSourceCommand{ apiType: helmChartType, object: helmChartAdapter{&sourcev1.HelmChart{}}, }.run, } func init() { reconcileSourceHelmChartCmd.Flags().BoolVar(&rhcArgs.syncHrWithSource, "with-source", false, "reconcile HelmChart source") reconcileSourceCmd.AddCommand(reconcileSourceHelmChartCmd) } func (obj helmChartAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } type reconcileHelmChartFlags struct { syncHrWithSource bool } var rhcArgs reconcileHelmChartFlags func (obj helmChartAdapter) reconcileSource() bool { return rhcArgs.syncHrWithSource } func (obj helmChartAdapter) getSource() (reconcileSource, sourceReference) { var cmd reconcileSource switch obj.Spec.SourceRef.Kind { case sourcev1.HelmRepositoryKind: cmd = reconcileCommand{ apiType: helmRepositoryType, object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, } case sourcev1.GitRepositoryKind: cmd = reconcileCommand{ apiType: gitRepositoryType, object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, } case sourcev1.BucketKind: cmd = reconcileCommand{ apiType: bucketType, object: bucketAdapter{&sourcev1.Bucket{}}, } } return cmd, sourceReference{ kind: obj.Spec.SourceRef.Kind, name: obj.Spec.SourceRef.Name, namespace: obj.Namespace, } } func (obj helmChartAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Reconcile a GitRepository source", Long: `The reconcile source command triggers a reconciliation of a GitRepository resource and waits for it to finish.`, Example: ` # Trigger a git pull for an existing source flux reconcile source git podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: reconcileCommand{ apiType: gitRepositoryType, object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, }.run, } func init() { reconcileSourceCmd.AddCommand(reconcileSourceGitCmd) } func (obj gitRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj gitRepositoryAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } func (obj gitRepositoryAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Reconcile a HelmRepository source", Long: `The reconcile source command triggers a reconciliation of a HelmRepository resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing source flux reconcile source helm podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: reconcileCommand{ apiType: helmRepositoryType, object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, }.run, } func init() { reconcileSourceCmd.AddCommand(reconcileSourceHelmCmd) } func (obj helmRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj helmRepositoryAdapter) successMessage() string { // HelmRepository of type OCI don't set an Artifact if obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { readyCondition := conditions.Get(obj.HelmRepository, meta.ReadyCondition) // This shouldn't happen, successMessage shouldn't be called if // object isn't ready if readyCondition == nil { return "" } return readyCondition.Message } return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } func (obj helmRepositoryAdapter) isStatic() bool { return obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI } ================================================ FILE: cmd/flux/reconcile_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Reconcile an OCIRepository", Long: `The reconcile source command triggers a reconciliation of an OCIRepository resource and waits for it to finish.`, Example: ` # Trigger a reconciliation for an existing source flux reconcile source oci podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: reconcileCommand{ apiType: ociRepositoryType, object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, }.run, } func init() { reconcileSourceCmd.AddCommand(reconcileSourceOCIRepositoryCmd) } func (obj ociRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } func (obj ociRepositoryAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } func (obj ociRepositoryAdapter) isStatic() bool { return false } ================================================ FILE: cmd/flux/reconcile_with_source.go ================================================ package main import ( "context" "fmt" "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/v2/internal/utils" ) type sourceReference struct { kind string name string namespace string } type reconcileWithSource interface { adapter reconcilable reconcileSource() bool getSource() (reconcileSource, sourceReference) } type reconcileSource interface { run(cmd *cobra.Command, args []string) error } type reconcileWithSourceCommand struct { apiType object reconcileWithSource force bool } func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("%s name is required", reconcile.kind) } name := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, reconcile.object.asClientObject()) if err != nil { return err } if reconcile.object.isSuspended() { return fmt.Errorf("resource is suspended") } if reconcile.object.reconcileSource() || reconcile.force { reconcileCmd, srcRef := reconcile.object.getSource() if reconcileCmd == nil { return fmt.Errorf("cannot reconcile source of kind %s", srcRef.kind) } nsCopy := *kubeconfigArgs.Namespace if srcRef.namespace != "" { *kubeconfigArgs.Namespace = srcRef.namespace } if err := reconcileCmd.run(nil, []string{srcRef.name}); err != nil { return err } *kubeconfigArgs.Namespace = nsCopy } lastHandledReconcileAt := reconcile.object.lastHandledReconcileRequest() logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.groupVersion.WithKind(reconcile.kind)); err != nil { return err } logger.Successf("%s annotated", reconcile.kind) logger.Waitingf("waiting for %s reconciliation", reconcile.kind) if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, reconciliationHandled(kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { return err } readyCond := apimeta.FindStatusCondition(reconcilableConditions(reconcile.object), meta.ReadyCondition) if readyCond == nil { return fmt.Errorf("status can't be determined") } if readyCond.Status != metav1.ConditionTrue { return fmt.Errorf("%s reconciliation failed: %s", reconcile.kind, readyCond.Message) } logger.Successf("%s", reconcile.object.successMessage()) return nil } ================================================ FILE: cmd/flux/resume.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "sort" "sync" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/v2/internal/utils" ) var resumeCmd = &cobra.Command{ Use: "resume", Short: "Resume suspended resources", Long: `The resume sub-commands resume a suspended resource.`, } type ResumeFlags struct { all bool wait bool } var resumeArgs ResumeFlags func init() { resumeCmd.PersistentFlags().BoolVarP(&resumeArgs.all, "all", "", false, "resume all resources in that namespace") resumeCmd.PersistentFlags().BoolVarP(&resumeArgs.wait, "wait", "", false, "waits for one resource to reconcile before moving to the next one") rootCmd.AddCommand(resumeCmd) } type resumable interface { adapter copyable statusable setUnsuspended() isStatic() bool successMessage() string } type resumeCommand struct { apiType client client.WithWatch list listResumable namespace string shouldReconcile bool } type listResumable interface { listAdapter resumeItem(i int) resumable } type reconcileResponse struct { resumable err error } func (resume resumeCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 && !resumeArgs.all { return fmt.Errorf("%s name is required", resume.humanKind) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } resume.client = kubeClient resume.namespace = *kubeconfigArgs.Namespace // require waiting for the object(s) if the user has not provided the --wait flag and gave exactly // one object to resume. This is necessary to maintain backwards compatibility with prior versions // of this command. Otherwise just follow the value of the --wait flag (including its default). resume.shouldReconcile = !resumeCmd.PersistentFlags().Changed("wait") && len(args) == 1 || resumeArgs.wait resumables, err := resume.getPatchedResumables(ctx, args) if err != nil { return err } var wg sync.WaitGroup wg.Add(len(resumables)) resultChan := make(chan reconcileResponse, len(resumables)) for _, r := range resumables { go func(res resumable) { defer wg.Done() resultChan <- resume.reconcile(ctx, res) }(r) } go func() { defer close(resultChan) wg.Wait() }() reconcileResps := make([]reconcileResponse, 0, len(resumables)) for c := range resultChan { reconcileResps = append(reconcileResps, c) } resume.printMessage(reconcileResps) // Return an error if any reconciliation failed var failedCount int for _, r := range reconcileResps { if r.resumable != nil && r.err != nil { failedCount++ } } if failedCount > 0 { return fmt.Errorf("reconciliation failed for %d %s(s)", failedCount, resume.kind) } return nil } // getPatchedResumables returns a list of the given resumable objects that have been patched to be resumed. // If the args slice is empty, it patches all resumable objects in the given namespace. func (resume *resumeCommand) getPatchedResumables(ctx context.Context, args []string) ([]resumable, error) { if len(args) < 1 { objs, err := resume.patch(ctx, args, []client.ListOption{ client.InNamespace(resume.namespace), }) if err != nil { return nil, fmt.Errorf("failed patching objects: %w", err) } return objs, nil } var resumables []resumable processed := make(map[string]struct{}, len(args)) for _, arg := range args { if _, has := processed[arg]; has { continue // skip object that user might have provided more than once } processed[arg] = struct{}{} objs, err := resume.patch(ctx, args, []client.ListOption{ client.InNamespace(resume.namespace), client.MatchingFields{ "metadata.name": arg, }, }) if err != nil { return nil, err } resumables = append(resumables, objs...) } return resumables, nil } // Patches resumable objects by setting their status to unsuspended. // Returns a slice of resumables that have been patched and any error encountered during patching. func (resume resumeCommand) patch(ctx context.Context, args []string, listOpts []client.ListOption) ([]resumable, error) { if err := resume.client.List(ctx, resume.list.asClientList(), listOpts...); err != nil { return nil, err } if resume.list.len() == 0 { if len(args) < 1 { logger.Failuref("no %s objects found in %s namespace", resume.kind, resume.namespace) } else { logger.Failuref("%s object '%s' not found in %s namespace", resume.kind, args[0], resume.namespace) } return nil, nil } var resumables []resumable for i := 0; i < resume.list.len(); i++ { obj := resume.list.resumeItem(i) logger.Actionf("resuming %s %s in %s namespace", resume.humanKind, obj.asClientObject().GetName(), resume.namespace) patch := client.MergeFrom(obj.deepCopyClientObject()) obj.setUnsuspended() if err := resume.client.Patch(ctx, obj.asClientObject(), patch); err != nil { return nil, err } resumables = append(resumables, obj) logger.Successf("%s resumed", resume.humanKind) } return resumables, nil } // Waits for resumable object to be reconciled and returns the object and any error encountered while waiting. // Returns an empty reconcileResponse, if shouldReconcile is false. func (resume resumeCommand) reconcile(ctx context.Context, res resumable) reconcileResponse { if !resume.shouldReconcile { return reconcileResponse{} } namespacedName := types.NamespacedName{ Name: res.asClientObject().GetName(), Namespace: resume.namespace, } logger.Waitingf("waiting for %s reconciliation", resume.kind) readyConditionFunc := isObjectReadyConditionFunc(resume.client, namespacedName, res.asClientObject()) if res.isStatic() { readyConditionFunc = isStaticObjectReadyConditionFunc(resume.client, namespacedName, res.asClientObject()) } if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { return reconcileResponse{ resumable: res, err: err, } } return reconcileResponse{ resumable: res, err: nil, } } // Sorts the given reconcileResponses by resumable name and prints the success/error message for each response. func (resume resumeCommand) printMessage(responses []reconcileResponse) { sort.Slice(responses, func(i, j int) bool { r1, r2 := responses[i], responses[j] if r1.resumable == nil || r2.resumable == nil { return false } return r1.asClientObject().GetName() <= r2.asClientObject().GetName() }) // Print success/error message. for _, r := range responses { if r.resumable == nil { continue } if r.err != nil { logger.Failuref("%s %s reconciliation failed: %s", resume.kind, r.asClientObject().GetName(), r.err.Error()) continue } logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName()) logger.Successf("%s", r.successMessage()) } } ================================================ FILE: cmd/flux/resume_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var resumeAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Resume a suspended Alert", Long: `The resume command marks a previously suspended Alert resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Alert flux resume alert main # Resume reconciliation for multiple Alerts flux resume alert main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: resumeCommand{ apiType: alertType, list: &alertListAdapter{¬ificationv1.AlertList{}}, }.run, } func init() { resumeCmd.AddCommand(resumeAlertCmd) } func (obj alertAdapter) getObservedGeneration() int64 { return 0 } func (obj alertAdapter) setUnsuspended() { obj.Alert.Spec.Suspend = false } func (obj alertAdapter) successMessage() string { return "Alert reconciliation completed" } func (a alertAdapter) isStatic() bool { return true } func (a alertListAdapter) resumeItem(i int) resumable { return &alertAdapter{&a.AlertList.Items[i]} } ================================================ FILE: cmd/flux/resume_alertprovider.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var resumeAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Resume a suspended Provider", Long: `The resume command marks a previously suspended Provider resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Provider flux resume alert-provider main # Resume reconciliation for multiple Providers flux resume alert-provider main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), RunE: resumeCommand{ apiType: alertProviderType, list: &alertProviderListAdapter{¬ificationv1.ProviderList{}}, }.run, } func init() { resumeCmd.AddCommand(resumeAlertProviderCmd) } func (obj alertProviderAdapter) getObservedGeneration() int64 { return 0 } func (obj alertProviderAdapter) setUnsuspended() { obj.Provider.Spec.Suspend = false } func (obj alertProviderAdapter) successMessage() string { return "Provider reconciliation completed" } func (a alertProviderAdapter) isStatic() bool { return true } func (a alertProviderListAdapter) resumeItem(i int) resumable { return &alertProviderAdapter{&a.ProviderList.Items[i]} } ================================================ FILE: cmd/flux/resume_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var resumeHrCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Resume a suspended HelmRelease", Long: `The resume command marks a previously suspended HelmRelease resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Helm release flux resume hr podinfo # Resume reconciliation for multiple Helm releases flux resume hr podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: resumeCommand{ apiType: helmReleaseType, list: helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, }.run, } func init() { resumeCmd.AddCommand(resumeHrCmd) } func (obj helmReleaseAdapter) getObservedGeneration() int64 { return obj.HelmRelease.Status.ObservedGeneration } func (obj helmReleaseAdapter) setUnsuspended() { obj.HelmRelease.Spec.Suspend = false } func (obj helmReleaseAdapter) successMessage() string { return fmt.Sprintf("applied revision %s", getHelmReleaseRevision(*obj.HelmRelease)) } func (a helmReleaseListAdapter) resumeItem(i int) resumable { return &helmReleaseAdapter{&a.HelmReleaseList.Items[i]} } ================================================ FILE: cmd/flux/resume_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var resumeImageCmd = &cobra.Command{ Use: "image", Short: "Resume image automation objects", Long: `The resume image sub-commands resume suspended image automation objects.`, } func init() { resumeCmd.AddCommand(resumeImageCmd) } ================================================ FILE: cmd/flux/resume_image_policy.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" "github.com/spf13/cobra" ) var resumeImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Resume an ImagePolicy", Long: `The resume image policy command resumes a suspended ImagePolicy resource.`, Example: ` # Resume a suspended image policy called 'alpine' flux resume image policy alpine`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: resumeCommand{ apiType: imagePolicyType, list: imagePolicyListAdapter{&imagev1.ImagePolicyList{}}, }.run, } func init() { resumeImageCmd.AddCommand(resumeImagePolicyCmd) } ================================================ FILE: cmd/flux/resume_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var resumeImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Resume a suspended ImageRepository", Long: `The resume command marks a previously suspended ImageRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing ImageRepository flux resume image repository alpine # Resume reconciliation for multiple ImageRepositories flux resume image repository alpine-1 alpine-2`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: resumeCommand{ apiType: imageRepositoryType, list: imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, }.run, } func init() { resumeImageCmd.AddCommand(resumeImageRepositoryCmd) } func (obj imageRepositoryAdapter) getObservedGeneration() int64 { return obj.ImageRepository.Status.ObservedGeneration } func (obj imageRepositoryAdapter) setUnsuspended() { obj.ImageRepository.Spec.Suspend = false } func (a imageRepositoryListAdapter) resumeItem(i int) resumable { return &imageRepositoryAdapter{&a.ImageRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/resume_image_updateauto.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" autov1 "github.com/fluxcd/image-automation-controller/api/v1" ) var resumeImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Resume a suspended ImageUpdateAutomation", Long: `The resume command marks a previously suspended ImageUpdateAutomation resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing ImageUpdateAutomation flux resume image update latest-images # Resume reconciliation for multiple ImageUpdateAutomations flux resume image update latest-images-1 latest-images-2`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: resumeCommand{ apiType: imageUpdateAutomationType, list: imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, }.run, } func init() { resumeImageCmd.AddCommand(resumeImageUpdateCmd) } func (obj imageUpdateAutomationAdapter) setUnsuspended() { obj.ImageUpdateAutomation.Spec.Suspend = false } func (obj imageUpdateAutomationAdapter) getObservedGeneration() int64 { return obj.ImageUpdateAutomation.Status.ObservedGeneration } func (a imageUpdateAutomationListAdapter) resumeItem(i int) resumable { return &imageUpdateAutomationAdapter{&a.ImageUpdateAutomationList.Items[i]} } ================================================ FILE: cmd/flux/resume_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var resumeKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Resume a suspended Kustomization", Long: `The resume command marks a previously suspended Kustomization resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Kustomization flux resume ks podinfo # Resume reconciliation for multiple Kustomizations flux resume ks podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: resumeCommand{ apiType: kustomizationType, list: kustomizationListAdapter{&kustomizev1.KustomizationList{}}, }.run, } func init() { resumeCmd.AddCommand(resumeKsCmd) } func (obj kustomizationAdapter) getObservedGeneration() int64 { return obj.Kustomization.Status.ObservedGeneration } func (obj kustomizationAdapter) setUnsuspended() { obj.Kustomization.Spec.Suspend = false } func (obj kustomizationAdapter) successMessage() string { return fmt.Sprintf("applied revision %s", obj.Status.LastAppliedRevision) } func (a kustomizationListAdapter) resumeItem(i int) resumable { return &kustomizationAdapter{&a.KustomizationList.Items[i]} } ================================================ FILE: cmd/flux/resume_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var resumeReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Resume a suspended Receiver", Long: `The resume command marks a previously suspended Receiver resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Receiver flux resume receiver main # Resume reconciliation for multiple Receivers flux resume receiver main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: resumeCommand{ apiType: receiverType, list: receiverListAdapter{¬ificationv1.ReceiverList{}}, }.run, } func init() { resumeCmd.AddCommand(resumeReceiverCmd) } func (obj receiverAdapter) getObservedGeneration() int64 { return obj.Receiver.Status.ObservedGeneration } func (obj receiverAdapter) setUnsuspended() { obj.Receiver.Spec.Suspend = false } func (obj receiverAdapter) successMessage() string { return "Receiver reconciliation completed" } func (a receiverListAdapter) resumeItem(i int) resumable { return &receiverAdapter{&a.ReceiverList.Items[i]} } ================================================ FILE: cmd/flux/resume_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var resumeSourceCmd = &cobra.Command{ Use: "source", Short: "Resume sources", Long: `The resume sub-commands resume a suspended source.`, } func init() { resumeCmd.AddCommand(resumeSourceCmd) } ================================================ FILE: cmd/flux/resume_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Resume a suspended Bucket", Long: `The resume command marks a previously suspended Bucket resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing Bucket flux resume source bucket podinfo # Resume reconciliation for multiple Buckets flux resume source bucket podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: resumeCommand{ apiType: bucketType, list: bucketListAdapter{&sourcev1.BucketList{}}, }.run, } func init() { resumeSourceCmd.AddCommand(resumeSourceBucketCmd) } func (obj bucketAdapter) getObservedGeneration() int64 { return obj.Bucket.Status.ObservedGeneration } func (obj bucketAdapter) setUnsuspended() { obj.Bucket.Spec.Suspend = false } func (a bucketListAdapter) resumeItem(i int) resumable { return &bucketAdapter{&a.BucketList.Items[i]} } ================================================ FILE: cmd/flux/resume_source_chart.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceHelmChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Resume a suspended HelmChart", Long: `The resume command marks a previously suspended HelmChart resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing HelmChart flux resume source chart podinfo # Resume reconciliation for multiple HelmCharts flux resume source chart podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: resumeCommand{ apiType: helmChartType, list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, }.run, } func init() { resumeSourceCmd.AddCommand(resumeSourceHelmChartCmd) } func (obj helmChartAdapter) getObservedGeneration() int64 { return obj.HelmChart.Status.ObservedGeneration } func (obj helmChartAdapter) setUnsuspended() { obj.HelmChart.Spec.Suspend = false } func (obj helmChartAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } func (a helmChartListAdapter) resumeItem(i int) resumable { return &helmChartAdapter{&a.HelmChartList.Items[i]} } ================================================ FILE: cmd/flux/resume_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Resume a suspended GitRepository", Long: `The resume command marks a previously suspended GitRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing GitRepository flux resume source git podinfo # Resume reconciliation for multiple GitRepositories flux resume source git podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: resumeCommand{ apiType: gitRepositoryType, list: gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, }.run, } func init() { resumeSourceCmd.AddCommand(resumeSourceGitCmd) } func (obj gitRepositoryAdapter) getObservedGeneration() int64 { return obj.GitRepository.Status.ObservedGeneration } func (obj gitRepositoryAdapter) setUnsuspended() { obj.GitRepository.Spec.Suspend = false } func (a gitRepositoryListAdapter) resumeItem(i int) resumable { return &gitRepositoryAdapter{&a.GitRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/resume_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Resume a suspended HelmRepository", Long: `The resume command marks a previously suspended HelmRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing HelmRepository flux resume source helm bitnami # Resume reconciliation for multiple HelmRepositories flux resume source helm bitnami-1 bitnami-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: resumeCommand{ apiType: helmRepositoryType, list: helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, }.run, } func init() { resumeSourceCmd.AddCommand(resumeSourceHelmCmd) } func (obj helmRepositoryAdapter) getObservedGeneration() int64 { return obj.HelmRepository.Status.ObservedGeneration } func (obj helmRepositoryAdapter) setUnsuspended() { obj.HelmRepository.Spec.Suspend = false } func (a helmRepositoryListAdapter) resumeItem(i int) resumable { return &helmRepositoryAdapter{&a.HelmRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/resume_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Resume a suspended OCIRepository", Long: `The resume command marks a previously suspended OCIRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing OCIRepository flux resume source oci podinfo # Resume reconciliation for multiple OCIRepositories flux resume source oci podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: resumeCommand{ apiType: ociRepositoryType, list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, }.run, } func init() { resumeSourceCmd.AddCommand(resumeSourceOCIRepositoryCmd) } func (obj ociRepositoryAdapter) getObservedGeneration() int64 { return obj.OCIRepository.Status.ObservedGeneration } func (obj ociRepositoryAdapter) setUnsuspended() { obj.OCIRepository.Spec.Suspend = false } func (a ociRepositoryListAdapter) resumeItem(i int) resumable { return &ociRepositoryAdapter{&a.OCIRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/source.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) // These are general-purpose adapters for attaching methods to, for // the various commands. The *List adapters implement len(), since // it's used in at least a couple of commands. // sourcev1.ociRepository var ociRepositoryType = apiType{ kind: sourcev1.OCIRepositoryKind, humanKind: "source oci", groupVersion: sourcev1.GroupVersion, } type ociRepositoryAdapter struct { *sourcev1.OCIRepository } func (a ociRepositoryAdapter) asClientObject() client.Object { return a.OCIRepository } func (a ociRepositoryAdapter) deepCopyClientObject() client.Object { return a.OCIRepository.DeepCopy() } // sourcev1b2.OCIRepositoryList type ociRepositoryListAdapter struct { *sourcev1.OCIRepositoryList } func (a ociRepositoryListAdapter) asClientList() client.ObjectList { return a.OCIRepositoryList } func (a ociRepositoryListAdapter) len() int { return len(a.OCIRepositoryList.Items) } // sourcev1.Bucket var bucketType = apiType{ kind: sourcev1.BucketKind, humanKind: "source bucket", groupVersion: sourcev1.GroupVersion, } type bucketAdapter struct { *sourcev1.Bucket } func (a bucketAdapter) asClientObject() client.Object { return a.Bucket } func (a bucketAdapter) deepCopyClientObject() client.Object { return a.Bucket.DeepCopy() } // sourcev1.BucketList type bucketListAdapter struct { *sourcev1.BucketList } func (a bucketListAdapter) asClientList() client.ObjectList { return a.BucketList } func (a bucketListAdapter) len() int { return len(a.BucketList.Items) } // sourcev1.HelmChart var helmChartType = apiType{ kind: sourcev1.HelmChartKind, humanKind: "source chart", groupVersion: sourcev1.GroupVersion, } type helmChartAdapter struct { *sourcev1.HelmChart } func (a helmChartAdapter) asClientObject() client.Object { return a.HelmChart } func (a helmChartAdapter) deepCopyClientObject() client.Object { return a.HelmChart.DeepCopy() } // sourcev1.HelmChartList type helmChartListAdapter struct { *sourcev1.HelmChartList } func (a helmChartListAdapter) asClientList() client.ObjectList { return a.HelmChartList } func (a helmChartListAdapter) len() int { return len(a.HelmChartList.Items) } // sourcev1.GitRepository var gitRepositoryType = apiType{ kind: sourcev1.GitRepositoryKind, humanKind: "source git", groupVersion: sourcev1.GroupVersion, } type gitRepositoryAdapter struct { *sourcev1.GitRepository } func (a gitRepositoryAdapter) asClientObject() client.Object { return a.GitRepository } func (a gitRepositoryAdapter) deepCopyClientObject() client.Object { return a.GitRepository.DeepCopy() } // sourcev1.GitRepositoryList type gitRepositoryListAdapter struct { *sourcev1.GitRepositoryList } func (a gitRepositoryListAdapter) asClientList() client.ObjectList { return a.GitRepositoryList } func (a gitRepositoryListAdapter) len() int { return len(a.GitRepositoryList.Items) } // sourcev1.HelmRepository var helmRepositoryType = apiType{ kind: sourcev1.HelmRepositoryKind, humanKind: "source helm", groupVersion: sourcev1.GroupVersion, } type helmRepositoryAdapter struct { *sourcev1.HelmRepository } func (a helmRepositoryAdapter) asClientObject() client.Object { return a.HelmRepository } func (a helmRepositoryAdapter) deepCopyClientObject() client.Object { return a.HelmRepository.DeepCopy() } // sourcev1.HelmRepositoryList type helmRepositoryListAdapter struct { *sourcev1.HelmRepositoryList } func (a helmRepositoryListAdapter) asClientList() client.ObjectList { return a.HelmRepositoryList } func (a helmRepositoryListAdapter) len() int { return len(a.HelmRepositoryList.Items) } // sourcev1.ExternalArtifact var externalArtifactType = apiType{ kind: sourcev1.ExternalArtifactKind, humanKind: "source external-artifact", groupVersion: sourcev1.GroupVersion, } type externalArtifactAdapter struct { *sourcev1.ExternalArtifact } func (a externalArtifactAdapter) asClientObject() client.Object { return a.ExternalArtifact } func (a externalArtifactAdapter) deepCopyClientObject() client.Object { return a.ExternalArtifact.DeepCopy() } // sourcev1.ExternalArtifactList type externalArtifactListAdapter struct { *sourcev1.ExternalArtifactList } func (a externalArtifactListAdapter) asClientList() client.ObjectList { return a.ExternalArtifactList } func (a externalArtifactListAdapter) len() int { return len(a.ExternalArtifactList.Items) } ================================================ FILE: cmd/flux/source_oci_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestSourceOCI(t *testing.T) { namespace := allocateNamespace("oci-test") del, err := execSetupTestNamespace(namespace) if err != nil { t.Fatal(err) } t.Cleanup(del) tmpl := map[string]string{"ns": namespace} cases := []struct { args string goldenFile string tmpl map[string]string }{ { "create source oci thrfg --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m", "testdata/oci/create_source_oci.golden", nil, }, { "get source oci thrfg", "testdata/oci/get_oci.golden", nil, }, { "reconcile source oci thrfg", "testdata/oci/reconcile_oci.golden", tmpl, }, { "suspend source oci thrfg", "testdata/oci/suspend_oci.golden", tmpl, }, { "resume source oci thrfg", "testdata/oci/resume_oci.golden", tmpl, }, { "delete source oci thrfg --silent", "testdata/oci/delete_oci.golden", tmpl, }, } for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) } } ================================================ FILE: cmd/flux/stats.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/cli-utils/pkg/kstatus/status" helmv2 "github.com/fluxcd/helm-controller/api/v2" autov1 "github.com/fluxcd/image-automation-controller/api/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/printers" ) var statsCmd = &cobra.Command{ Use: "stats", Args: cobra.NoArgs, Short: "Stats of Flux reconciles", Long: withPreviewNote(`The stats command prints a report of Flux custom resources present on a cluster, including their reconcile status and the amount of cumulative storage used for each source type`), Example: ` # Print the stats report for a namespace flux stats --namespace default # Print the stats report for the whole cluster flux stats -A`, RunE: runStatsCmd, } type StatsFlags struct { allNamespaces bool } var statsArgs StatsFlags func init() { statsCmd.PersistentFlags().BoolVarP(&statsArgs.allNamespaces, "all-namespaces", "A", false, "list the statistics for objects across all namespaces") rootCmd.AddCommand(statsCmd) } func runStatsCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } types := []metav1.GroupVersionKind{ { Kind: sourcev1.GitRepositoryKind, Version: sourcev1.GroupVersion.Version, Group: sourcev1.GroupVersion.Group, }, { Kind: sourcev1.OCIRepositoryKind, Version: sourcev1.GroupVersion.Version, Group: sourcev1.GroupVersion.Group, }, { Kind: sourcev1.HelmRepositoryKind, Version: sourcev1.GroupVersion.Version, Group: sourcev1.GroupVersion.Group, }, { Kind: sourcev1.HelmChartKind, Version: sourcev1.GroupVersion.Version, Group: sourcev1.GroupVersion.Group, }, { Kind: sourcev1.BucketKind, Version: sourcev1.GroupVersion.Version, Group: sourcev1.GroupVersion.Group, }, { Kind: kustomizev1.KustomizationKind, Version: kustomizev1.GroupVersion.Version, Group: kustomizev1.GroupVersion.Group, }, { Kind: helmv2.HelmReleaseKind, Version: helmv2.GroupVersion.Version, Group: helmv2.GroupVersion.Group, }, { Kind: notificationv1b3.AlertKind, Version: notificationv1b3.GroupVersion.Version, Group: notificationv1b3.GroupVersion.Group, }, { Kind: notificationv1b3.ProviderKind, Version: notificationv1b3.GroupVersion.Version, Group: notificationv1b3.GroupVersion.Group, }, { Kind: notificationv1.ReceiverKind, Version: notificationv1.GroupVersion.Version, Group: notificationv1.GroupVersion.Group, }, { Kind: autov1.ImageUpdateAutomationKind, Version: autov1.GroupVersion.Version, Group: autov1.GroupVersion.Group, }, { Kind: imagev1.ImagePolicyKind, Version: imagev1.GroupVersion.Version, Group: imagev1.GroupVersion.Group, }, { Kind: imagev1.ImageRepositoryKind, Version: imagev1.GroupVersion.Version, Group: imagev1.GroupVersion.Group, }, } header := []string{"Reconcilers", "Running", "Failing", "Suspended", "Storage"} var rows [][]string for _, t := range types { var total int var suspended int var failing int var totalSize int64 list := unstructured.UnstructuredList{ Object: map[string]interface{}{ "apiVersion": t.Group + "/" + t.Version, "kind": t.Kind, }, } scope := client.InNamespace("") if !statsArgs.allNamespaces { scope = client.InNamespace(*kubeconfigArgs.Namespace) } if err := kubeClient.List(ctx, &list, scope); err == nil { total = len(list.Items) for _, item := range list.Items { if s, _, _ := unstructured.NestedBool(item.Object, "spec", "suspend"); s { suspended++ } if obj, err := status.GetObjectWithConditions(item.Object); err == nil { for _, cond := range obj.Status.Conditions { if cond.Type == "Ready" && cond.Status == corev1.ConditionFalse { failing++ } } } if size, found, _ := unstructured.NestedInt64(item.Object, "status", "artifact", "size"); found { totalSize += size } } } rows = append(rows, []string{ t.Kind, formatInt(total - suspended), formatInt(failing), formatInt(suspended), formatSize(totalSize), }) } err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows) if err != nil { return err } return nil } func formatInt(i int) string { return fmt.Sprintf("%d", i) } func formatSize(b int64) string { if b == 0 { return "-" } const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) } ================================================ FILE: cmd/flux/status.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/fluxcd/cli-utils/pkg/object" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) // statusable is used to see if a resource is considered ready in the usual way type statusable interface { adapter // this is implemented by ObjectMeta GetGeneration() int64 getObservedGeneration() int64 } // oldConditions represents the deprecated API which is sunsetting. type oldConditions interface { // this is usually implemented by GOTK API objects because it's used by pkg/apis/meta GetStatusConditions() *[]metav1.Condition } func buildComponentObjectRefs(components ...string) ([]object.ObjMetadata, error) { var objRefs []object.ObjMetadata for _, deployment := range components { objRefs = append(objRefs, object.ObjMetadata{ Namespace: *kubeconfigArgs.Namespace, Name: deployment, GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, }) } return objRefs, nil } ================================================ FILE: cmd/flux/suspend.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "errors" "fmt" "github.com/spf13/cobra" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/v2/internal/utils" ) var suspendCmd = &cobra.Command{ Use: "suspend", Short: "Suspend resources", Long: `The suspend sub-commands suspend the reconciliation of a resource.`, } type SuspendFlags struct { all bool } var suspendArgs SuspendFlags func init() { suspendCmd.PersistentFlags().BoolVarP(&suspendArgs.all, "all", "", false, "suspend all resources in that namespace") rootCmd.AddCommand(suspendCmd) } type suspendable interface { adapter copyable isSuspended() bool setSuspended() } type suspendCommand struct { apiType list listSuspendable object suspendable } type listSuspendable interface { listAdapter item(i int) suspendable } func (suspend suspendCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 && !suspendArgs.all { return fmt.Errorf("%s name is required", suspend.humanKind) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if len(args) < 1 && suspendArgs.all { listOpts := []client.ListOption{ client.InNamespace(*kubeconfigArgs.Namespace), } if err := suspend.patch(ctx, kubeClient, listOpts); err != nil { return err } return nil } processed := make(map[string]struct{}, len(args)) for _, arg := range args { if _, has := processed[arg]; has { continue // skip object that user might have provided more than once } processed[arg] = struct{}{} listOpts := []client.ListOption{ client.InNamespace(*kubeconfigArgs.Namespace), client.MatchingFields{ "metadata.name": arg, }, } if err := suspend.patch(ctx, kubeClient, listOpts); err != nil { if err == ErrNoObjectsFound { logger.Failuref("%s %s not found in %s namespace", suspend.kind, arg, *kubeconfigArgs.Namespace) } else { logger.Failuref("failed suspending %s %s in %s namespace: %s", suspend.kind, arg, *kubeconfigArgs.Namespace, err.Error()) } } } return nil } var ErrNoObjectsFound = errors.New("no objects found") func (suspend suspendCommand) patch(ctx context.Context, kubeClient client.WithWatch, listOpts []client.ListOption) error { if err := kubeClient.List(ctx, suspend.list.asClientList(), listOpts...); err != nil { return err } if suspend.list.len() == 0 { return ErrNoObjectsFound } 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) 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) } return nil } ================================================ FILE: cmd/flux/suspend_alert.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var suspendAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Suspend reconciliation of Alert", Long: `The suspend command disables the reconciliation of a Alert resource.`, Example: ` # Suspend reconciliation for an existing Alert flux suspend alert main # Suspend reconciliation for multiple Alerts flux suspend alert main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: suspendCommand{ apiType: alertType, object: &alertAdapter{¬ificationv1.Alert{}}, list: &alertListAdapter{¬ificationv1.AlertList{}}, }.run, } func init() { suspendCmd.AddCommand(suspendAlertCmd) } func (obj alertAdapter) isSuspended() bool { return obj.Alert.Spec.Suspend } func (obj alertAdapter) setSuspended() { obj.Alert.Spec.Suspend = true } func (a alertListAdapter) item(i int) suspendable { return &alertAdapter{&a.AlertList.Items[i]} } ================================================ FILE: cmd/flux/suspend_alertprovider.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var suspendAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Suspend reconciliation of Provider", Long: `The suspend command disables the reconciliation of a Provider resource.`, Example: ` # Suspend reconciliation for an existing Provider flux suspend alert-provider main # Suspend reconciliation for multiple Providers flux suspend alert-providers main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), RunE: suspendCommand{ apiType: alertProviderType, object: &alertProviderAdapter{¬ificationv1.Provider{}}, list: &alertProviderListAdapter{¬ificationv1.ProviderList{}}, }.run, } func init() { suspendCmd.AddCommand(suspendAlertProviderCmd) } func (obj alertProviderAdapter) isSuspended() bool { return obj.Provider.Spec.Suspend } func (obj alertProviderAdapter) setSuspended() { obj.Provider.Spec.Suspend = true } func (a alertProviderListAdapter) item(i int) suspendable { return &alertProviderAdapter{&a.ProviderList.Items[i]} } ================================================ FILE: cmd/flux/suspend_helmrelease.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var suspendHrCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Suspend reconciliation of HelmRelease", Long: `The suspend command disables the reconciliation of a HelmRelease resource.`, Example: ` # Suspend reconciliation for an existing Helm release flux suspend hr podinfo # Suspend reconciliation for multiple Helm releases flux suspend hr podinfo-1 podinfo-2 `, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: suspendCommand{ apiType: helmReleaseType, object: &helmReleaseAdapter{&helmv2.HelmRelease{}}, list: &helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, }.run, } func init() { suspendCmd.AddCommand(suspendHrCmd) } func (obj helmReleaseAdapter) isSuspended() bool { return obj.HelmRelease.Spec.Suspend } func (obj helmReleaseAdapter) setSuspended() { obj.HelmRelease.Spec.Suspend = true } func (a helmReleaseListAdapter) item(i int) suspendable { return &helmReleaseAdapter{&a.HelmReleaseList.Items[i]} } ================================================ FILE: cmd/flux/suspend_image.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var suspendImageCmd = &cobra.Command{ Use: "image", Short: "Suspend image automation objects", Long: `The suspend image sub-commands suspend the reconciliation of an image automation object.`, } func init() { suspendCmd.AddCommand(suspendImageCmd) } ================================================ FILE: cmd/flux/suspend_image_policy.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" "github.com/spf13/cobra" ) var suspendImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Suspend an ImagePolicy", Long: `The suspend image policy command suspends the reconciliation of an ImagePolicy resource.`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), RunE: suspendCommand{ apiType: imagePolicyType, list: imagePolicyListAdapter{&imagev1.ImagePolicyList{}}, }.run, } func init() { suspendImageCmd.AddCommand(suspendImagePolicyCmd) } ================================================ FILE: cmd/flux/suspend_image_repository.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" ) var suspendImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Suspend reconciliation of an ImageRepository", Long: `The suspend image repository command disables the reconciliation of a ImageRepository resource.`, Example: ` # Suspend reconciliation for an existing ImageRepository flux suspend image repository alpine # Suspend reconciliation for multiple ImageRepositories flux suspend image repository alpine-1 alpine-2`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: suspendCommand{ apiType: imageRepositoryType, object: imageRepositoryAdapter{&imagev1.ImageRepository{}}, list: &imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, }.run, } func init() { suspendImageCmd.AddCommand(suspendImageRepositoryCmd) } func (obj imageRepositoryAdapter) isSuspended() bool { return obj.ImageRepository.Spec.Suspend } func (obj imageRepositoryAdapter) setSuspended() { obj.ImageRepository.Spec.Suspend = true } func (a imageRepositoryListAdapter) item(i int) suspendable { return &imageRepositoryAdapter{&a.ImageRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/suspend_image_updateauto.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" autov1 "github.com/fluxcd/image-automation-controller/api/v1" ) var suspendImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Suspend reconciliation of an ImageUpdateAutomation", Long: `The suspend image update command disables the reconciliation of a ImageUpdateAutomation resource.`, Example: ` # Suspend reconciliation for an existing ImageUpdateAutomation flux suspend image update latest-images # Suspend reconciliation for multiple ImageUpdateAutomations flux suspend image update latest-images-1 latest-images-2`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: suspendCommand{ apiType: imageUpdateAutomationType, object: imageUpdateAutomationAdapter{&autov1.ImageUpdateAutomation{}}, list: &imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, }.run, } func init() { suspendImageCmd.AddCommand(suspendImageUpdateCmd) } func (update imageUpdateAutomationAdapter) isSuspended() bool { return update.ImageUpdateAutomation.Spec.Suspend } func (update imageUpdateAutomationAdapter) setSuspended() { update.ImageUpdateAutomation.Spec.Suspend = true } func (a imageUpdateAutomationListAdapter) item(i int) suspendable { return &imageUpdateAutomationAdapter{&a.ImageUpdateAutomationList.Items[i]} } ================================================ FILE: cmd/flux/suspend_kustomization.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var suspendKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Suspend reconciliation of Kustomization", Long: `The suspend command disables the reconciliation of a Kustomization resource.`, Example: ` # Suspend reconciliation for an existing Kustomization flux suspend ks podinfo # Suspend reconciliation for multiple Kustomizations flux suspend ks podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: suspendCommand{ apiType: kustomizationType, object: kustomizationAdapter{&kustomizev1.Kustomization{}}, list: &kustomizationListAdapter{&kustomizev1.KustomizationList{}}, }.run, } func init() { suspendCmd.AddCommand(suspendKsCmd) } func (obj kustomizationAdapter) isSuspended() bool { return obj.Kustomization.Spec.Suspend } func (obj kustomizationAdapter) setSuspended() { obj.Kustomization.Spec.Suspend = true } func (a kustomizationListAdapter) item(i int) suspendable { return &kustomizationAdapter{&a.KustomizationList.Items[i]} } ================================================ FILE: cmd/flux/suspend_receiver.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var suspendReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Suspend reconciliation of Receiver", Long: `The suspend command disables the reconciliation of a Receiver resource.`, Example: ` # Suspend reconciliation for an existing Receiver flux suspend receiver main # Suspend reconciliation for multiple Receivers flux suspend receiver main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: suspendCommand{ apiType: receiverType, object: &receiverAdapter{¬ificationv1.Receiver{}}, list: &receiverListAdapter{¬ificationv1.ReceiverList{}}, }.run, } func init() { suspendCmd.AddCommand(suspendReceiverCmd) } func (obj receiverAdapter) isSuspended() bool { return obj.Receiver.Spec.Suspend } func (obj receiverAdapter) setSuspended() { obj.Receiver.Spec.Suspend = true } func (a receiverListAdapter) item(i int) suspendable { return &receiverAdapter{&a.ReceiverList.Items[i]} } ================================================ FILE: cmd/flux/suspend_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var suspendSourceCmd = &cobra.Command{ Use: "source", Short: "Suspend sources", Long: `The suspend sub-commands suspend the reconciliation of a source.`, } func init() { suspendCmd.AddCommand(suspendSourceCmd) } ================================================ FILE: cmd/flux/suspend_source_bucket.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Suspend reconciliation of a Bucket", Long: `The suspend command disables the reconciliation of a Bucket resource.`, Example: ` # Suspend reconciliation for an existing Bucket flux suspend source bucket podinfo # Suspend reconciliation for multiple Buckets flux suspend source bucket podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: suspendCommand{ apiType: bucketType, object: bucketAdapter{&sourcev1.Bucket{}}, list: bucketListAdapter{&sourcev1.BucketList{}}, }.run, } func init() { suspendSourceCmd.AddCommand(suspendSourceBucketCmd) } func (obj bucketAdapter) isSuspended() bool { return obj.Bucket.Spec.Suspend } func (obj bucketAdapter) setSuspended() { obj.Bucket.Spec.Suspend = true } func (a bucketListAdapter) item(i int) suspendable { return &bucketAdapter{&a.BucketList.Items[i]} } ================================================ FILE: cmd/flux/suspend_source_chart.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceHelmChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Suspend reconciliation of a HelmChart", Long: `The suspend command disables the reconciliation of a HelmChart resource.`, Example: ` # Suspend reconciliation for an existing HelmChart flux suspend source chart podinfo # Suspend reconciliation for multiple HelmCharts flux suspend source chart podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: suspendCommand{ apiType: helmChartType, object: helmChartAdapter{&sourcev1.HelmChart{}}, list: helmChartListAdapter{&sourcev1.HelmChartList{}}, }.run, } func init() { suspendSourceCmd.AddCommand(suspendSourceHelmChartCmd) } func (obj helmChartAdapter) isSuspended() bool { return obj.HelmChart.Spec.Suspend } func (obj helmChartAdapter) setSuspended() { obj.HelmChart.Spec.Suspend = true } func (a helmChartListAdapter) item(i int) suspendable { return &helmChartAdapter{&a.HelmChartList.Items[i]} } ================================================ FILE: cmd/flux/suspend_source_git.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Suspend reconciliation of a GitRepository", Long: `The suspend command disables the reconciliation of a GitRepository resource.`, Example: ` # Suspend reconciliation for an existing GitRepository flux suspend source git podinfo # Suspend reconciliation for multiple GitRepositories flux suspend source git podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: suspendCommand{ apiType: gitRepositoryType, object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, list: gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, }.run, } func init() { suspendSourceCmd.AddCommand(suspendSourceGitCmd) } func (obj gitRepositoryAdapter) isSuspended() bool { return obj.GitRepository.Spec.Suspend } func (obj gitRepositoryAdapter) setSuspended() { obj.GitRepository.Spec.Suspend = true } func (a gitRepositoryListAdapter) item(i int) suspendable { return &gitRepositoryAdapter{&a.GitRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/suspend_source_helm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Suspend reconciliation of a HelmRepository", Long: `The suspend command disables the reconciliation of a HelmRepository resource.`, Example: ` # Suspend reconciliation for an existing HelmRepository flux suspend source helm bitnami # Suspend reconciliation for multiple HelmRepositories flux suspend source helm bitnami-1 bitnami-2 `, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: suspendCommand{ apiType: helmRepositoryType, object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, list: helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, }.run, } func init() { suspendSourceCmd.AddCommand(suspendSourceHelmCmd) } func (obj helmRepositoryAdapter) isSuspended() bool { return obj.HelmRepository.Spec.Suspend } func (obj helmRepositoryAdapter) setSuspended() { obj.HelmRepository.Spec.Suspend = true } func (a helmRepositoryListAdapter) item(i int) suspendable { return &helmRepositoryAdapter{&a.HelmRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/suspend_source_oci.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceOCIRepositoryCmd = &cobra.Command{ Use: "oci [name]", Short: "Suspend reconciliation of an OCIRepository", Long: `The suspend command disables the reconciliation of an OCIRepository resource.`, Example: ` # Suspend reconciliation for an existing OCIRepository flux suspend source oci podinfo # Suspend reconciliation for multiple OCIRepositories flux suspend source oci podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), RunE: suspendCommand{ apiType: ociRepositoryType, object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, }.run, } func init() { suspendSourceCmd.AddCommand(suspendSourceOCIRepositoryCmd) } func (obj ociRepositoryAdapter) isSuspended() bool { return obj.OCIRepository.Spec.Suspend } func (obj ociRepositoryAdapter) setSuspended() { obj.OCIRepository.Spec.Suspend = true } func (a ociRepositoryListAdapter) item(i int) suspendable { return &ociRepositoryAdapter{&a.OCIRepositoryList.Items[i]} } ================================================ FILE: cmd/flux/tag.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var tagCmd = &cobra.Command{ Use: "tag", Short: "Tag artifacts", Long: `The tag command is used to tag OCI artifacts.`, } func init() { rootCmd.AddCommand(tagCmd) } ================================================ FILE: cmd/flux/tag_artifact.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/spf13/cobra" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/flags" ) var tagArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Tag artifact", Long: `The tag artifact command creates tags for the given OCI artifact. The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`, Example: ` # Tag an artifact version as latest flux tag artifact oci://ghcr.io/org/manifests/app:v0.0.1 --tag latest `, RunE: tagArtifactCmdRun, } type tagArtifactFlags struct { tags []string creds string provider flags.SourceOCIProvider } var tagArtifactArgs = newTagArtifactFlags() func newTagArtifactFlags() tagArtifactFlags { return tagArtifactFlags{ provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), } } func init() { tagArtifactCmd.Flags().StringSliceVar(&tagArtifactArgs.tags, "tag", nil, "tag name") tagArtifactCmd.Flags().StringVar(&tagArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") tagArtifactCmd.Flags().Var(&tagArtifactArgs.provider, "provider", tagArtifactArgs.provider.Description()) tagCmd.AddCommand(tagArtifactCmd) } func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact name is required") } ociURL := args[0] if len(tagArtifactArgs.tags) < 1 { return fmt.Errorf("--tag is required") } url, err := oci.ParseArtifactURL(ociURL) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() opts := oci.DefaultOptions() if tagArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { logger.Actionf("logging in to registry with provider credentials") opt, _, err := loginWithProvider(ctx, url, tagArtifactArgs.provider.String()) if err != nil { return fmt.Errorf("error during login with provider: %w", err) } opts = append(opts, opt) } ociClient := oci.NewClient(opts) if tagArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && tagArtifactArgs.creds != "" { logger.Actionf("logging in to registry with credentials") if err := ociClient.LoginWithCredentials(tagArtifactArgs.creds); err != nil { return fmt.Errorf("could not login with credentials: %w", err) } } logger.Actionf("tagging artifact") for _, tag := range tagArtifactArgs.tags { img, err := ociClient.Tag(ctx, url, tag) if err != nil { return fmt.Errorf("tagging artifact failed: %w", err) } logger.Successf("artifact tagged as %s", img) } return nil } ================================================ FILE: cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: podinfo spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.3 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml ================================================ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: podinfo spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: podinfo minReplicas: 2 maxReplicas: 4 metrics: - type: Resource resource: name: cpu target: type: Utilization # scale up if usage is above # 99% of the requested CPU (100m) averageUtilization: 99 ================================================ FILE: cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ./deployment.yaml - ./hpa.yaml ================================================ FILE: cmd/flux/testdata/build-kustomization/ignore/.sourceignore ================================================ # exclude all /* ================================================ FILE: cmd/flux/testdata/build-kustomization/ignore/configmap.yaml ================================================ apiVersion: v1 data: var: test kind: ConfigMap metadata: name: configmap_ignore ================================================ FILE: cmd/flux/testdata/build-kustomization/ignore/not_deployable/ignore_svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: do_not_deploy spec: type: ClusterIP selector: app: podinfo ports: - name: http port: 9898 protocol: TCP targetPort: http - port: 9999 targetPort: grpc protocol: TCP name: grpc ================================================ FILE: cmd/flux/testdata/build-kustomization/ignore/secret.yaml ================================================ apiVersion: v1 data: token: KipTT1BTKio= kind: Secret metadata: name: secret_ignore type: Opaque ================================================ FILE: cmd/flux/testdata/build-kustomization/my-app/configmap.yaml ================================================ apiVersion: v1 data: var: test kind: ConfigMap metadata: name: my-app ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: podinfo spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/dockerconfigjson-sops-secret.yaml ================================================ 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 ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml ================================================ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: podinfo spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: podinfo minReplicas: 2 maxReplicas: 4 metrics: - type: Resource resource: name: cpu target: type: Utilization # scale up if usage is above # 99% of the requested CPU (100m) averageUtilization: 99 ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ./deployment.yaml - ./hpa.yaml - ./service.yaml - ./dockerconfigjson-sops-secret.yaml - ./stringdata-secret.yaml secretGenerator: - files: - token=token.encrypted name: podinfo-token - literals: - username=admin - password=1f2d1e2e67df name: db-user-pass ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: podinfo spec: type: ClusterIP selector: app: podinfo ports: - name: http port: 9898 protocol: TCP targetPort: http - port: 9999 targetPort: grpc protocol: TCP name: grpc ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/stringdata-secret.yaml ================================================ 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 ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo/token.encrypted ================================================ { "data": "ENC[AES256_GCM,data:oBe5PlPmfQCUUc4sqKImjw==,iv:MLLEW15QC9kRdVVagJnzLCSk0xZGWIpAeTfHzyxT10g=,tag:K3GkBCGS+ut4Tpk6ndb0CA==,type:str]", "sops": { "kms": null, "gcp_kms": null, "azure_kv": null, "hc_vault": null, "age": [ { "recipient": "age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce", "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+ IFgyNTUxOSA1L2RpZHRrK1FSVmYrd1Va\nY0hxWFQzSDBsT1k3WjNtYmU1QmliaDJycXlNCnF1YjdNOThVbVNvMG9rNS9ZUXZw\nMnV0bnRUMGNtejFPbzM4U2UzWkszeVkKLS0tIGJ6UGhxMUV3YmVJTHlJSUJpRVRZ\nVjd0RVRadU8wekxXTHIrYUplYkN2aEEK0I/ MCEtXRk+b/N2G1JF3vHQT24dShWYD\nw+JIUSA3aLf2sv0zr2MdUEdVWBJoM8nT4D4xVbBORD+669W+9nDeSw==\n-----END AGE ENCRYPTED FILE-----\n" } ], "lastmodified": "2021-11-26T16:34:51Z", "mac": "ENC[AES256_GCM,data:COGzf5YCHNNP6z4JaEKrjN3M8f5+Q1uKUKTMHwj388/ICmLyi2sSrTmj7PP+X7M9jTVwa8wVgYTpNLiVJx+LcxqvIXM0Tyo+/Cu1zrfao98aiACP8+TSEDiFQNtEus23H+d/X1hqMwRHDI3kQ+ 6scgEGnqY57r3RDSA3E8EhHr4=,iv:LxitVIYm8srZVqFueJh9loClA44Y2Z3XAVYmxesMmOg=,tag:Y8qFD8UGlDfwNSv7xlcn6A==,type:str]", "pgp": null, "unencrypted_suffix": "_unencrypted", "version": "3.7.1" } } ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml ================================================ --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 5m0s path: ./kustomize force: true prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default postBuild: substitute: cluster_env: "prod" cluster_region: "eu-central-1" ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-result.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: minReadySeconds: 3 progressDeadlineSeconds: 60 revisionHistoryLimit: 5 selector: matchLabels: app: podinfo strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate template: metadata: annotations: prometheus.io/port: "9797" prometheus.io/scrape: "true" labels: app: podinfo spec: containers: - command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: '#34577c' image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 name: podinfod ports: - containerPort: 9898 name: http protocol: TCP - containerPort: 9797 name: http-metrics protocol: TCP - containerPort: 9999 name: grpc protocol: TCP readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: maxReplicas: 4 metrics: - resource: name: cpu target: averageUtilization: 99 type: Utilization type: Resource minReplicas: 2 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: podinfo --- apiVersion: v1 kind: Service metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: ports: - name: http port: 9898 protocol: TCP targetPort: http - name: grpc port: 9999 protocol: TCP targetPort: grpc selector: app: podinfo 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: '**SOPS**' username: '**SOPS**' type: kubernetes.io/basic-auth --- apiVersion: v1 data: token: KipTT1BTKio= kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-token-77t89m9b67 namespace: default type: Opaque --- apiVersion: v1 data: password: MWYyZDFlMmU2N2Rm username: YWRtaW4= kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: db-user-pass-bkbd782d2c namespace: default type: Opaque --- ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-source.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 30s ref: branch: master url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-with-ignore-result.yaml ================================================ apiVersion: v1 data: var: test kind: ConfigMap metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: configmap_ignore namespace: default --- apiVersion: v1 data: token: KipTT1BTKio= kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: secret_ignore namespace: default type: Opaque --- ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-with-my-app/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ./my-app.yaml ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-with-my-app/my-app.yaml ================================================ --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: my-app spec: interval: 5m0s path: ./my-app force: true prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-with-my-app-result.yaml ================================================ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: my-app namespace: default spec: force: true interval: 5m0s path: ./my-app prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default --- apiVersion: v1 data: var: test kind: ConfigMap metadata: labels: kustomize.toolkit.fluxcd.io/name: my-app kustomize.toolkit.fluxcd.io/namespace: default name: my-app namespace: default --- ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-with-var-substitution-result.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: environment: prod kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} region: eu-central-1 name: podinfo namespace: default spec: minReadySeconds: 3 progressDeadlineSeconds: 60 revisionHistoryLimit: 5 selector: matchLabels: app: podinfo strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate template: metadata: annotations: prometheus.io/port: "9797" prometheus.io/scrape: "true" labels: app: podinfo spec: containers: - command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: '#34577c' image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 name: podinfod ports: - containerPort: 9898 name: http protocol: TCP - containerPort: 9797 name: http-metrics protocol: TCP - containerPort: 9999 name: grpc protocol: TCP readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi --- apiVersion: v1 data: cluster.json: | { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "iteration": 1636369574387, "links": [], "panels": [ { "datasource": "${DS_PROMETHEUS}", "description": "", "fieldConfig": { "defaults": { "decimals": 0, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null }, { "color": "red", "value": 100 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 }, "id": 24, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "last" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "7.5.5", "targets": [ { "exemplar": true, "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"Kustomization|HelmRelease\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"Kustomization|HelmRelease\"})", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "Cluster Reconcilers", "type": "stat" }, { "collapsed": false, "datasource": "${DS_PROMETHEUS}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "id": 15, "panels": [], "title": "Status", "type": "row" } ], "refresh": "", "schemaVersion": 27, "style": "light", "tags": [ "flux" ], "time": { "from": "now-15m", "to": "now" }, "timepicker": { "refresh_intervals": [ "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ] }, "timezone": "", "title": "Flux Cluster Stats", "uid": "flux-cluster", "version": 1 } kind: ConfigMap metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} kustomize.toolkit.fluxcd.io/substitute: disabled name: flux-grafana-dashboards-kt8md725kf namespace: default --- ================================================ FILE: cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: minReadySeconds: 3 progressDeadlineSeconds: 60 revisionHistoryLimit: 5 selector: matchLabels: app: podinfo strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate template: metadata: annotations: prometheus.io/port: "9797" prometheus.io/scrape: "true" labels: app: podinfo spec: containers: - command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: '#34577c' image: ghcr.io/stefanprodan/podinfo:6.0.3 imagePullPolicy: IfNotPresent livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 name: podinfod ports: - containerPort: 9898 name: http protocol: TCP - containerPort: 9797 name: http-metrics protocol: TCP - containerPort: 9999 name: grpc protocol: TCP readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: maxReplicas: 4 metrics: - resource: name: cpu target: averageUtilization: 99 type: Utilization type: Resource minReplicas: 2 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: podinfo --- ================================================ FILE: cmd/flux/testdata/build-kustomization/var-substitution/cluster.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "iteration": 1636369574387, "links": [], "panels": [ { "datasource": "${DS_PROMETHEUS}", "description": "", "fieldConfig": { "defaults": { "decimals": 0, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null }, { "color": "red", "value": 100 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 }, "id": 24, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "last" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "7.5.5", "targets": [ { "exemplar": true, "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"Kustomization|HelmRelease\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"Kustomization|HelmRelease\"})", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "Cluster Reconcilers", "type": "stat" }, { "collapsed": false, "datasource": "${DS_PROMETHEUS}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "id": 15, "panels": [], "title": "Status", "type": "row" } ], "refresh": "", "schemaVersion": 27, "style": "light", "tags": [ "flux" ], "time": { "from": "now-15m", "to": "now" }, "timepicker": { "refresh_intervals": [ "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ] }, "timezone": "", "title": "Flux Cluster Stats", "uid": "flux-cluster", "version": 1 } ================================================ FILE: cmd/flux/testdata/build-kustomization/var-substitution/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: environment: ${cluster_env:=dev} region: ${cluster_region} name: podinfo spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/build-kustomization/var-substitution/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ./deployment.yaml generatorOptions: labels: kustomize.toolkit.fluxcd.io/substitute: disabled configMapGenerator: - name: flux-grafana-dashboards files: - cluster.json ================================================ FILE: cmd/flux/testdata/check/check_pre.golden ================================================ ► checking prerequisites ✔ Kubernetes {{ .serverVersion }} >=1.33.0-0 ✔ prerequisites checks passed ================================================ FILE: cmd/flux/testdata/cluster_info/gitrepositories.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.12.0 name: gitrepositories.source.toolkit.fluxcd.io spec: group: source.toolkit.fluxcd.io names: kind: GitRepository listKind: GitRepositoryList plural: gitrepositories shortNames: - gitrepo singular: gitrepository scope: Namespaced versions: - additionalPrinterColumns: - jsonPath: .spec.url name: URL type: string - jsonPath: .metadata.creationTimestamp name: Age type: date - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string - jsonPath: .status.conditions[?(@.type=="Ready")].message name: Status type: string name: v1 schema: openAPIV3Schema: description: GitRepository is the Schema for the gitrepositories API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: GitRepositorySpec specifies the required configuration to produce an Artifact for a Git repository. properties: ignore: description: Ignore overrides the set of excluded patterns in the .sourceignore format (which is the same as .gitignore). If not provided, a default will be used, consult the documentation for your version to find out what those are. type: string include: description: Include specifies a list of GitRepository resources which Artifacts should be included in the Artifact produced for this GitRepository. items: description: GitRepositoryInclude specifies a local reference to a GitRepository which Artifact (sub-)contents must be included, and where they should be placed. properties: fromPath: description: FromPath specifies the path to copy contents from, defaults to the root of the Artifact. type: string repository: description: GitRepositoryRef specifies the GitRepository which Artifact contents must be included. properties: name: description: Name of the referent. type: string required: - name type: object toPath: description: ToPath specifies the path to copy contents to, defaults to the name of the GitRepositoryRef. type: string required: - repository type: object type: array interval: description: Interval at which the GitRepository URL is checked for updates. This interval is approximate and may be subject to jitter to ensure efficient use of resources. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string proxySecretRef: description: ProxySecretRef specifies the Secret containing the proxy configuration to use while communicating with the Git server. properties: name: description: Name of the referent. type: string required: - name type: object recurseSubmodules: description: RecurseSubmodules enables the initialization of all submodules within the GitRepository as cloned from the URL, using their default settings. type: boolean ref: description: Reference specifies the Git reference to resolve and monitor for changes, defaults to the 'master' branch. properties: branch: description: Branch to check out, defaults to 'master' if no other field is defined. type: string commit: description: "Commit SHA to check out, takes precedence over all reference fields. \n This can be combined with Branch to shallow clone the branch, in which the commit is expected to exist." type: string name: description: "Name of the reference to check out; takes precedence over Branch, Tag and SemVer. \n It must be a valid Git reference: https://git-scm.com/docs/git-check-ref-format#_description Examples: \"refs/heads/main\", \"refs/tags/v0.1.0\", \"refs/pull/420/head\", \"refs/merge-requests/1/head\"" type: string semver: description: SemVer tag expression to check out, takes precedence over Tag. type: string tag: description: Tag to check out, takes precedence over Branch. type: string type: object secretRef: description: SecretRef specifies the Secret containing authentication credentials for the GitRepository. For HTTPS repositories the Secret must contain 'username' and 'password' fields for basic auth or 'bearerToken' field for token auth. For SSH repositories the Secret must contain 'identity' and 'known_hosts' fields. properties: name: description: Name of the referent. type: string required: - name type: object suspend: description: Suspend tells the controller to suspend the reconciliation of this GitRepository. type: boolean timeout: default: 60s description: Timeout for Git operations like cloning, defaults to 60s. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ type: string url: description: URL specifies the Git repository URL, it can be an HTTP/S or SSH address. pattern: ^(http|https|ssh)://.*$ type: string verify: description: Verification specifies the configuration to verify the Git commit signature(s). properties: mode: default: HEAD description: "Mode specifies which Git object(s) should be verified. \n The variants \"head\" and \"HEAD\" both imply the same thing, i.e. verify the commit that the HEAD of the Git repository points to. The variant \"head\" solely exists to ensure backwards compatibility." enum: - head - HEAD - Tag - TagAndHEAD type: string secretRef: description: SecretRef specifies the Secret containing the public keys of trusted Git authors. properties: name: description: Name of the referent. type: string required: - name type: object required: - secretRef type: object required: - interval - url type: object status: default: observedGeneration: -1 description: GitRepositoryStatus records the observed state of a Git repository. properties: artifact: description: Artifact represents the last successful GitRepository reconciliation. properties: digest: description: Digest is the digest of the file in the form of ':'. pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ type: string lastUpdateTime: description: LastUpdateTime is the timestamp corresponding to the last update of the Artifact. format: date-time type: string metadata: additionalProperties: type: string description: Metadata holds upstream information such as OCI annotations. type: object path: description: Path is the relative file path of the Artifact. It can be used to locate the file in the root of the Artifact storage on the local file system of the controller managing the Source. type: string revision: description: Revision is a human-readable identifier traceable in the origin source system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. type: string size: description: Size is the number of bytes in the file. format: int64 type: integer url: description: URL is the HTTP address of the Artifact as exposed by the controller managing the Source. It can be used to retrieve the Artifact for consumption, e.g. by another controller applying the Artifact contents. type: string required: - lastUpdateTime - path - revision - url type: object conditions: description: Conditions holds the conditions for the GitRepository. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: lastTransitionTime: description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array includedArtifacts: description: IncludedArtifacts contains a list of the last successfully included Artifacts as instructed by GitRepositorySpec.Include. items: description: Artifact represents the output of a Source reconciliation. properties: digest: description: Digest is the digest of the file in the form of ':'. pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ type: string lastUpdateTime: description: LastUpdateTime is the timestamp corresponding to the last update of the Artifact. format: date-time type: string metadata: additionalProperties: type: string description: Metadata holds upstream information such as OCI annotations. type: object path: description: Path is the relative file path of the Artifact. It can be used to locate the file in the root of the Artifact storage on the local file system of the controller managing the Source. type: string revision: description: Revision is a human-readable identifier traceable in the origin source system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. type: string size: description: Size is the number of bytes in the file. format: int64 type: integer url: description: URL is the HTTP address of the Artifact as exposed by the controller managing the Source. It can be used to retrieve the Artifact for consumption, e.g. by another controller applying the Artifact contents. type: string required: - lastUpdateTime - path - revision - url type: object type: array lastHandledReconcileAt: description: LastHandledReconcileAt holds the value of the most recent reconcile request value, so a change of the annotation value can be detected. type: string observedGeneration: description: ObservedGeneration is the last observed generation of the GitRepository object. format: int64 type: integer observedIgnore: description: ObservedIgnore is the observed exclusion patterns used for constructing the source artifact. type: string observedInclude: description: ObservedInclude is the observed list of GitRepository resources used to produce the current Artifact. items: description: GitRepositoryInclude specifies a local reference to a GitRepository which Artifact (sub-)contents must be included, and where they should be placed. properties: fromPath: description: FromPath specifies the path to copy contents from, defaults to the root of the Artifact. type: string repository: description: GitRepositoryRef specifies the GitRepository which Artifact contents must be included. properties: name: description: Name of the referent. type: string required: - name type: object toPath: description: ToPath specifies the path to copy contents to, defaults to the name of the GitRepositoryRef. type: string required: - repository type: object type: array observedRecurseSubmodules: description: ObservedRecurseSubmodules is the observed resource submodules configuration used to produce the current Artifact. type: boolean sourceVerificationMode: description: SourceVerificationMode is the last used verification mode indicating which Git object(s) have been verified. type: string type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: cmd/flux/testdata/create_hr/basic.yaml ================================================ --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: {{ .fluxns }} spec: chart: spec: chart: podinfo reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo interval: 1m0s ================================================ FILE: cmd/flux/testdata/create_hr/hc_basic.yaml ================================================ --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: {{ .fluxns }} spec: chartRef: kind: HelmChart name: podinfo interval: 1m0s ================================================ FILE: cmd/flux/testdata/create_hr/or_basic.yaml ================================================ --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: {{ .fluxns }} spec: chartRef: kind: OCIRepository name: podinfo interval: 1m0s ================================================ FILE: cmd/flux/testdata/create_hr/setup-source.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 1m0s provider: generic type: oci url: oci://ghcr.io/stefanprodan/charts --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 1m0s chart: podinfo sourceRef: kind: HelmRepository name: podinfo --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 10m url: oci://ghcr.io/stefanprodan/manifests/podinfo ref: tag: latest ================================================ FILE: cmd/flux/testdata/create_secret/git/ecdsa-password.private ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABA9i7hZ7m UBPxF7GuUswZiXAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz dHAyNTYAAABBBOOAQfhwylg6WhJRXha2K1dJY5BG12nsS7YyFlKPAovOAC8d8rTOzjjDWu kG24JVRrXoWUN6eWWvxDvMd5pZT1wAAADAepYZPGTQnH6genhU+y9aD1GFO8BsODIllxEq T8n2eLAmPWYLv4HhgtmbqtsOmTE9sVM5ynYj/dAX1SreoGeoMEVFIt1cYtWgyuJccEiK4t 1JQLFLMKnBRTY+yIdD6gX7tLRRL/jqzMR4XZF5/Yf48lvU4h+ljuOitWH3ea9142izl2Wk eXAfeoezJaDntZUUEYvKMI5U6iWVni+c7vOcJFTZgBeV4i54ua06tY5mE/mWARldiSDtJG yGEOankDnf -----END OPENSSH PRIVATE KEY----- ================================================ FILE: cmd/flux/testdata/create_secret/git/ecdsa.private ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTuiu+gGwLIu9E5J4vRshoIBHScKx4Y f9oniWMsLFHXq5p9GJ/eb9Cr3jgNACnGOIGOqlwBQvP5rCJuaJ0pCRKUAAAAuNE1GtvRNR rbAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO6K76AbAsi70Tkn i9GyGggEdJwrHhh/2ieJYywsUdermn0Yn95v0KveOA0AKcY4gY6qXAFC8/msIm5onSkJEp QAAAAhAPaO6PiN+1238KMrHg34M7XdftGypt2/UKEz2L2Pf40yAAAAH3NvbXRvY2hpb255 ZWt3ZXJlQFNvbXRvY2hpcy1NQlA= -----END OPENSSH PRIVATE KEY----- ================================================ FILE: cmd/flux/testdata/create_secret/git/git-bearer-token.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: bearer-token-auth namespace: my-namespace stringData: bearerToken: ghp_baR2qnFF0O41WlucePL3udt2N9vVZS4R0hAS ================================================ FILE: cmd/flux/testdata/create_secret/git/git-ssh-secret-password.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: podinfo-auth namespace: my-namespace stringData: identity: | -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABA9i7hZ7m UBPxF7GuUswZiXAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz dHAyNTYAAABBBOOAQfhwylg6WhJRXha2K1dJY5BG12nsS7YyFlKPAovOAC8d8rTOzjjDWu kG24JVRrXoWUN6eWWvxDvMd5pZT1wAAADAepYZPGTQnH6genhU+y9aD1GFO8BsODIllxEq T8n2eLAmPWYLv4HhgtmbqtsOmTE9sVM5ynYj/dAX1SreoGeoMEVFIt1cYtWgyuJccEiK4t 1JQLFLMKnBRTY+yIdD6gX7tLRRL/jqzMR4XZF5/Yf48lvU4h+ljuOitWH3ea9142izl2Wk eXAfeoezJaDntZUUEYvKMI5U6iWVni+c7vOcJFTZgBeV4i54ua06tY5mE/mWARldiSDtJG yGEOankDnf -----END OPENSSH PRIVATE KEY----- identity.pub: | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOOAQfhwylg6WhJRXha2K1dJY5BG12nsS7YyFlKPAovOAC8d8rTOzjjDWukG24JVRrXoWUN6eWWvxDvMd5pZT1w= known_hosts: github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= password: password ================================================ FILE: cmd/flux/testdata/create_secret/git/git-ssh-secret.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: podinfo-auth namespace: my-namespace stringData: identity: | -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTuiu+gGwLIu9E5J4vRshoIBHScKx4Y f9oniWMsLFHXq5p9GJ/eb9Cr3jgNACnGOIGOqlwBQvP5rCJuaJ0pCRKUAAAAuNE1GtvRNR rbAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO6K76AbAsi70Tkn i9GyGggEdJwrHhh/2ieJYywsUdermn0Yn95v0KveOA0AKcY4gY6qXAFC8/msIm5onSkJEp QAAAAhAPaO6PiN+1238KMrHg34M7XdftGypt2/UKEz2L2Pf40yAAAAH3NvbXRvY2hpb255 ZWt3ZXJlQFNvbXRvY2hpcy1NQlA= -----END OPENSSH PRIVATE KEY----- identity.pub: | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO6K76AbAsi70Tkni9GyGggEdJwrHhh/2ieJYywsUdermn0Yn95v0KveOA0AKcY4gY6qXAFC8/msIm5onSkJEpQ= known_hosts: github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= ================================================ FILE: cmd/flux/testdata/create_secret/git/secret-ca-crt.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: ca-crt namespace: my-namespace stringData: ca.crt: ca-data password: my-password username: my-username ================================================ FILE: cmd/flux/testdata/create_secret/git/secret-git-basic.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: podinfo-auth namespace: my-namespace stringData: password: my-password username: my-username ================================================ FILE: cmd/flux/testdata/create_secret/githubapp/secret-with-baseurl.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: appinfo namespace: my-namespace stringData: githubAppBaseURL: www.example.com/api/v3 githubAppID: "1" githubAppInstallationID: "2" githubAppPrivateKey: |- -----BEGIN RSA PRIVATE KEY----- YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= -----END RSA PRIVATE KEY----- type: Opaque ================================================ FILE: cmd/flux/testdata/create_secret/githubapp/secret.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: appinfo namespace: my-namespace stringData: githubAppID: "1" githubAppInstallationOwner: my-org githubAppPrivateKey: |- -----BEGIN RSA PRIVATE KEY----- YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= -----END RSA PRIVATE KEY----- type: Opaque ================================================ FILE: cmd/flux/testdata/create_secret/githubapp/test-private-key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm LsXqL2iZvVvCH0FiDwBIfxAMhl6fnPzuQsZBiRLPdD67jubPseN1P5JBRw3WTton Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T MROFeZmxBHYon1Y4Rw+jCSXovNyHbMBpMI67nwIDAQABAoIBAC4UrkusU8r7ilFu w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB FkhEBrwXKnh499iiO/SUo+7kaq0WLQ7mQ2Q9wpMmkkjnr0tgydAno/uNNITSaqmk YcE2CgWILk+uiVNseHnOU2frG7k2RJZtdDo8GNI6pQWFlwU/NsQoJBrtEDyYVkap Fa2RLLByuyge7bWh2o1hjEx2w2ZpIhosQRyDs1sPXP5TI92RzOcx1CZaTZ6V5E+T zSde2KD/ilB1YUhaAv0qaBHLTGBvVgxut1eIyvqVC+ArWoqJ7rQTst+Arp3UAiIm ihlXNkECgYEA3abZJZuVarHPlAqRYkprs0O+DrP6sPlmVQp+nq8y3Qg00U+N7AuP Y1riLo3gWq7LajkGTygWLmru2mhWsETxt+R4BtnREUq8kDEoCfEwPlHfqfphvBZL j5eL60QTKAqSOVqMgIzqJyxa5FGgPGqWpLDLopyVeoyNdZwcuCQzFgkCgYEA25dm PLv7VoJ2pr6l5IEwH++naL2McuCcwhW/CeaVb/IuSCFFMwb40zRrlqp6IB5VkMhm MkvaCGIAH+lfJrtTSujFaOIGFy+0ZwP+LNqHUKih14y8Qv9dEP0kaXkAD3fO3Y97 Nj+Q2c06JpojgBKBMwVvT7M53w9KEoNKpoKBbmcCgYBelHyiRJJsdbVKyXuiAnmU g/qMkZYOgE1/SjwfgEjm8kJ/cj/wEjq8PaK4FMhAScf46p5blpJoei6zucQL8U9n lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s dNhf6gsKwQD3x4aluKSn6QKBgD8HbvBAKV6P4vIiFzS0QvWtpeKam2EDHI+h+WsP nD77QoG/EPvpjJS9/KWgZRPz6U+0M5V0y73MZVzkbbVT/uwfgF2G91lXAr4Kfuh5 w1LWDm09+8WSIk9KgYMoBceAqH+b9DU7hrMFrKkO/cr0Mijr1somv6B3MG83WUcB qCEDAoGACl8ClvMJR2uNWdaWnCz9tyPdHYgEusJ0OIP+WUY2ToYQWSlA0zNpc21Y lbD102oXw9lUefVI0McyQIN9J58ewDC79AG7gU/fTSt6F75OeFLOJmoedQo33Y+s bUytJtOhHbLRNxwgalhjBUNWICrDktqJmumNOEOOPBqVz7RGwUg= -----END RSA PRIVATE KEY----- ================================================ FILE: cmd/flux/testdata/create_secret/helm/secret-helm.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: helm-secret namespace: my-namespace stringData: password: my-password username: my-username type: kubernetes.io/basic-auth ================================================ FILE: cmd/flux/testdata/create_secret/notation/invalid-trust-policy.json ================================================ { "version": "1.0", "trustPolicies": [{}] } ================================================ FILE: cmd/flux/testdata/create_secret/notation/invalid.json ================================================ "" ================================================ FILE: cmd/flux/testdata/create_secret/notation/secret-ca-crt.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: notation-config namespace: my-namespace stringData: ca.crt: ca-data-crt trustpolicy.json: | { "version": "1.0", "trustPolicies": [ { "name": "fluxcd.io", "registryScopes": [ "*" ], "signatureVerification": { "level" : "strict" }, "trustStores": [ "ca:fluxcd.io" ], "trustedIdentities": [ "*" ] } ] } ================================================ FILE: cmd/flux/testdata/create_secret/notation/secret-ca-multi.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: notation-config namespace: my-namespace stringData: ca.crt: ca-data-crt ca.pem: ca-data-pem trustpolicy.json: | { "version": "1.0", "trustPolicies": [ { "name": "fluxcd.io", "registryScopes": [ "*" ], "signatureVerification": { "level" : "strict" }, "trustStores": [ "ca:fluxcd.io" ], "trustedIdentities": [ "*" ] } ] } ================================================ FILE: cmd/flux/testdata/create_secret/notation/secret-ca-pem.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: notation-config namespace: my-namespace stringData: ca.pem: ca-data-pem trustpolicy.json: | { "version": "1.0", "trustPolicies": [ { "name": "fluxcd.io", "registryScopes": [ "*" ], "signatureVerification": { "level" : "strict" }, "trustStores": [ "ca:fluxcd.io" ], "trustedIdentities": [ "*" ] } ] } ================================================ FILE: cmd/flux/testdata/create_secret/notation/test-ca.crt ================================================ -----BEGIN CERTIFICATE----- MIIDbDCCAlSgAwIBAgIUP7zhmTw5XTWLcgBGkBEsErMOkz4wDQYJKoZIhvcNAQEL BQAwWjELMAkGA1UEBhMCUk8xCzAJBgNVBAgMAkJVMRIwEAYDVQQHDAlCdWNoYXJl c3QxDzANBgNVBAoMBk5vdGFyeTEZMBcGA1UEAwwQc3RlZmFucHJvZGFuLmNvbTAe Fw0yNDAyMjUxMDAyMzZaFw0yOTAyMjQxMDAyMzZaMFoxCzAJBgNVBAYTAlJPMQsw CQYDVQQIDAJCVTESMBAGA1UEBwwJQnVjaGFyZXN0MQ8wDQYDVQQKDAZOb3Rhcnkx GTAXBgNVBAMMEHN0ZWZhbnByb2Rhbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDtH4oPi3SyX/DGv6NdjIvmApvD9eeSgsmHdwpAly8T9D2me+fx Z+wRNJmq4aq/A1anX+Sg28iwHzV+1WKpsHnjYzDAJSEYP2S8A5H1nGRKUoibdijw C3QBh5C75rjF/tmZVSX/Vgbf3HJJEsF4WUxWabLxoV2QLo7UlEsQd9+bSeKNMncx 1+E6FdbRCrYo90iobvZJ8K/S2zCWq/JTeHfTnmSEDhx6nMJcaSjvMPn3zyauWcQw dDpkcaGiJ64fEJRT2OFxXv9u+vDmIMKzo/Wjbd+IzFj6YY4VisK88aU7tmDelnk5 gQB9eu62PFoaVsYJp4VOhblFKvGJpQwbWB9BAgMBAAGjKjAoMA4GA1UdDwEB/wQE AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEA 6x+C6hAIbLwMvkNx4K5p7Qe/pLQR0VwQFAw10yr/5KSN+YKFpon6pQ0TebL7qll+ uBGZvtQhN6v+DlnVqB7lvJKd+89isgirkkews5KwuXg7Gv5UPIugH0dXISZU8DMJ 7J4oKREv5HzdFmfsUfNlQcfyVTjKL6UINXfKGdqNNxXxR9b4a1TY2JcmEhzBTHaq ZqX6HK784a0dB7aHgeFrFwPCCP4M684Hs7CFbk3jo2Ef4ljnB5AyWpe8pwCLMdRt UjSjL5xJWVQvRU+STQsPr6SvpokPCG4rLQyjgeYYk4CCj5piSxbSUZFavq8v1y7Y m91USVqfeUX7ZzjDxPHE2A== -----END CERTIFICATE----- ================================================ FILE: cmd/flux/testdata/create_secret/notation/test-ca2.crt ================================================ -----BEGIN CERTIFICATE----- MIIDbDCCAlSgAwIBAgIUP7zhmTw5XTWLcgBGkBEsErMOkz4wDQYJKoZIhvcNAQEL BQAwWjELMAkGA1UEBhMCUk8xCzAJBgNVBAgMAkJVMRIwEAYDVQQHDAlCdWNoYXJl c3QxDzANBgNVBAoMBk5vdGFyeTEZMBcGA1UEAwwQc3RlZmFucHJvZGFuLmNvbTAe Fw0yNDAyMjUxMDAyMzZaFw0yOTAyMjQxMDAyMzZaMFoxCzAJBgNVBAYTAlJPMQsw CQYDVQQIDAJCVTESMBAGA1UEBwwJQnVjaGFyZXN0MQ8wDQYDVQQKDAZOb3Rhcnkx GTAXBgNVBAMMEHN0ZWZhbnByb2Rhbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDtH4oPi3SyX/DGv6NdjIvmApvD9eeSgsmHdwpAly8T9D2me+fx Z+wRNJmq4aq/A1anX+Sg28iwHzV+1WKpsHnjYzDAJSEYP2S8A5H1nGRKUoibdijw C3QBh5C75rjF/tmZVSX/Vgbf3HJJEsF4WUxWabLxoV2QLo7UlEsQd9+bSeKNMncx 1+E6FdbRCrYo90iobvZJ8K/S2zCWq/JTeHfTnmSEDhx6nMJcaSjvMPn3zyauWcQw dDpkcaGiJ64fEJRT2OFxXv9u+vDmIMKzo/Wjbd+IzFj6YY4VisK88aU7tmDelnk5 gQB9eu62PFoaVsYJp4VOhblFKvGJpQwbWB9BAgMBAAGjKjAoMA4GA1UdDwEB/wQE AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEA 6x+C6hAIbLwMvkNx4K5p7Qe/pLQR0VwQFAw10yr/5KSN+YKFpon6pQ0TebL7qll+ uBGZvtQhN6v+DlnVqB7lvJKd+89isgirkkews5KwuXg7Gv5UPIugH0dXISZU8DMJ 7J4oKREv5HzdFmfsUfNlQcfyVTjKL6UINXfKGdqNNxXxR9b4a1TY2JcmEhzBTHaq ZqX6HK784a0dB7aHgeFrFwPCCP4M684Hs7CFbk3jo2Ef4ljnB5AyWpe8pwCLMdRt UjSjL5xJWVQvRU+STQsPr6SvpokPCG4rLQyjgeYYk4CCj5piSxbSUZFavq8v1y7Y m91USVqfeUX7ZzjDxPHE2A== -----END CERTIFICATE----- ================================================ FILE: cmd/flux/testdata/create_secret/notation/test-trust-policy.json ================================================ { "version": "1.0", "trustPolicies": [ { "name": "fluxcd.io", "registryScopes": [ "*" ], "signatureVerification": { "level" : "strict" }, "trustStores": [ "ca:fluxcd.io" ], "trustedIdentities": [ "*" ] } ] } ================================================ FILE: cmd/flux/testdata/create_secret/oci/create-secret.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: ghcr namespace: my-namespace stringData: .dockerconfigjson: |- { "auths": { "ghcr.io": { "username": "stefanprodan", "password": "password", "auth": "c3RlZmFucHJvZGFuOnBhc3N3b3Jk" } } } type: kubernetes.io/dockerconfigjson ================================================ FILE: cmd/flux/testdata/create_secret/proxy/secret-proxy.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: proxy-secret namespace: my-namespace stringData: address: https://my-proxy.com password: my-password username: my-username type: Opaque ================================================ FILE: cmd/flux/testdata/create_secret/tls/secret-tls.yaml ================================================ --- apiVersion: v1 kind: Secret metadata: name: certs namespace: my-namespace stringData: ca.crt: | -----BEGIN CERTIFICATE----- MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ 4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O 1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb OD8EjjCMY69RMO0= -----END CERTIFICATE----- tls.crt: | -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUT84jeO/ncOrqI+FY05Fzbg8Ed7MwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA4MDgxNDQyMzVaFw0yMjA4 MDgxNDQyMzVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDn/rPsZ74oypiwCzLlx57zplTiCi/WLSF+MmLGuTvM EQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgSj4YKULCMwwOl45hQPdCTEPJvUhCm M+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigxM9mlg58AlBhVJqn8q64wd/kC/W/K JTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0KYQAhKjEto6ZrYGisfwU1gt3l8M7 sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPps+x/rGFplsPMsXEFFiwvR1+FJZwz lg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7C9V0sjb5trT2lEqZlP2dRSJYt7aJ 1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVkPo2IeEXy5xrR7DyonOQ6Yes0KOCm JB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBCOMXpUESuVZIw8YK+Vd6AExGPPwZ4 n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8rgpbJpPnS+VgryQ/raUQjqDzDCuE 9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFGUB/xsdrcO4yXjqHnAe0yLf8epDjC hh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkOLaVVb9ggPV2SNAHbN4A+St/iRYR5 awIDAQABo1MwUTAdBgNVHQ4EFgQUzMaCqVM30EZFfTeNUIJ5fNPAhaQwHwYDVR0j BBgwFoAUzMaCqVM30EZFfTeNUIJ5fNPAhaQwDwYDVR0TAQH/BAUwAwEB/zANBgkq hkiG9w0BAQsFAAOCAgEAVmk1rXtVkYR1Vs2Va/xrUaGXlFznhPU/Fft44kiEkkLp mLVelWyAqvXYioqssZwuZnTjGz0DQPqzJjqwuGy4CHwPLmhCtfHplrbWo8a0ivYC cL20KfZsG941siUh7LGBjTsq6mWBf2ytlFmg/fg93SgmqcEUAUcdps0JpZD8lgWB ZMstfr6E3jaEus3OsvDD6hJNYZ5clJ5+ynLoWZ99A9JC0U46hmIZpRjbdSvasKpD XrXTdpzyL/Do3znXE/yfoHv4//Rj2CpPHJLYRCIzvuf1mo1fWd53FjHvrbUvaHFz CGuZROd4dC4Rx5nZw2ogIYvJ8m6HpIDkL3pBNSQJtIsvAYEQcotJoa5D/e9fu2Wr +og37oCY4OXzViEBQvyxKD4cajNco1fgGKEaFROADwr3JceGI7Anq5W+xdUvAGNM QuGeCueqNyrJ0CbQ1zEhwgpk/VYfB0u9m0bjMellRlKMdojby+FDCJtAJesx9no4 SQXyx+aNHhj3qReysjGNwZvBk1IHL04HAT+ogNiYhTl1J/YON4MB5UN6Y2PxP6uG KvJGPigx4fAwfR/d78o5ngwoH9m+8FUg8+qllJ8XgIbl/VXKTk3G4ceOm4eBmrel DwWuBhELSjtXWPWhMlkiebgejDbAear53Lia2Cc43zx/KuhMHBTlKY/vY4F2YiI= -----END CERTIFICATE----- tls.key: | -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDn/rPsZ74oypiw CzLlx57zplTiCi/WLSF+MmLGuTvMEQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgS j4YKULCMwwOl45hQPdCTEPJvUhCmM+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigx M9mlg58AlBhVJqn8q64wd/kC/W/KJTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0 KYQAhKjEto6ZrYGisfwU1gt3l8M7sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPp s+x/rGFplsPMsXEFFiwvR1+FJZwzlg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7 C9V0sjb5trT2lEqZlP2dRSJYt7aJ1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVk Po2IeEXy5xrR7DyonOQ6Yes0KOCmJB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBC OMXpUESuVZIw8YK+Vd6AExGPPwZ4n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8 rgpbJpPnS+VgryQ/raUQjqDzDCuE9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFG UB/xsdrcO4yXjqHnAe0yLf8epDjChh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkO LaVVb9ggPV2SNAHbN4A+St/iRYR5awIDAQABAoICAQCTxuixQ/wbW8IbEWcgeyHD LkaPndGO6jyVeF73GvL+MDRFuj558NvpNLfqzvTWVf9AnQGMd5Xs9oGegRHu7Csp 3ucp+moBYv7DT14+jtXQKOgGJpDqSqfS1RUKb/TBRXNDLGy02UScziWoAdE33zmf UraVNwW8z1crxKA3yVw2Na++UqhGQlVLAbfXucqnJLVtNWKpkVQlezUgcfmFovsm Iut+9MjI6/sZAqdXTLKuCKo0XjWzNKwnRecE0CYsCwzc80MvFYEiwQi1C0kwoouC iOi8MKM/jDok+5/a3nQ7X+/ho5sbApNCJpfSXAK9YOJ3ju93+RjNuvORfp4/sW3W OGXw6X30Ym7WS/7oYuwEILyqdyNOvKU7a+17d/W/YA60NOdA4iJI3aTfYFMD3l14 Da+D/wkTlEN3Ye7GN21A9AsZwWWiT9G5FOxWWVv7nTPG+Ix5ewehQWt/3DxhSizR inMBizL5xpwx9LRWHnXX277lChYmPFAAMXINl1hnX6s0EY9pSDHN0IddibJkNKBD m1CN37rqxoXQz4zoAyJGfQVkakqe16ayqI9yuQwO6AUkZcD5DYQdz9QYOTnYrQc6 6haC3D0Fmqg1s4v+6gpxZA/qTri0gVl/v/NN4Mk2/qWtK33imOedgD+5LXhZdBgJ Mqn53AErG/AT622jvSb5UQKCAQEA/DTGLh0Ct97PCm+c+PxRFyieaHNJLWENKyxp HoWGHfp2Bvt2Vphoi7GpRCM/yta4vCZgZmeWTQ0yBg6iPVPRA6Ho5hqh9OkUYVoh prL3JsIU20jTutYjo2aefO4qXnJfkkXxNO2FElUHDTwtWdlGJQKvlUJwTv6xO19v bQQkhZSpri6gIpi5Nkm2SGEtDofRJ+F6ThbQibEatL6DR00dh39MYQz+tZP5olzn kX5bHEBWB7gy+YxTGF8FdlCSQTBBtNSKsAv3Cxj4qEHm+fu09vnH6fOZKenT2nXD 5QE/RpgQzLV1TumCjqLzqwp7bbzH+4mjsXpF3KHBZwnhMnDIRwKCAQEA63wYzjBy no0GBBz0hOWrOwQ/AjUHfi47o3Xvl4RBjZclM171HKH7oMCnQvVKTNq8jvakCZjc UI6i+H4R6aokiFS2xGbC2H3ZlSMFNwhb2xUs/C4Nr7JSOWZBtDy5QBspUsp26f7m 9VNVRzCmnxWV9be/1TxHDzDhslNlL5TMejbMorWnrtNG41KWwGtwvv2gApr3894j eJNOh0WGfsMkXUM6+4v4WcCGrdV8Cr6Nvu96ZZe2PWu2dANtAfnxqogXXCoFE6r1 vie7hFSfJ2QR/vEbanED4pYGTtGYP1oseScx0u0hLhGLGccVBUNZlRbox4rIOELI v9MLuiOL4YX7vQKCAQAGzMl3HtMe8AP3DRFXaT4qeK7ktA8KCS7YtibTatg14LXj 9E25gfx3n7+nlae3qVhrwkEhIbPcuflaTnSzYJonFet4oMkzGEGzakG0A+lEA0Ga s/j5daKaWj71sVo1F7JZ+EbLnYfT+bTp93BllsUcZFkllhf/GUDgD++qKc1uSJbW mm044ZNE0nH2u6ACX0kVYS/yAQ14WO0WaHiTqJGeQKFnkHkhni7B4O1hb923AkkP hjjhn5Xx90Xnbb6zwUBURtLCcmAjzXWO29AFd3Lmoc9xEF9V0PckUb6JYyI4ngr9 6fqSuRsLC3u0ZeD0EX322zwtodVWYIodZBfNS1srAoIBAQCjTUPGeUKDQTjS0WGg Z8T/AErRtQSlNFqXWMn2QPlUv2RE460HVi2xpOhZPtFvyqDIY7IOFbtzAfdya7rw V9VN1bGJMdodV+jzy31qVJmerGit2SIUnYz30TnvS80L78oQZ+dfDi4MIuYYoFxs JgQAipS1wz9kAXoCuGKLRJ0og6gVjfPjARE/w55XgiqFyEyWgfFBZOMkUsM6e7Rx Y9Jr+puEpeRsGV9MXafPq6WQq3It0a/HmFLG0TlfDX3RzN6mQ12R7hTM8bDQa/6S yorQSVPB1O3kzDVDo4X5KQd+XPfoVhmUYQYdsjmZlMMi6Og0uMFwgp/Epw6S3uO6 WbfhAoIBAQCOp4iIc87GyxWL8u6HrJaqmFlqkfou0hI+y9h6FfzsBYU6y3+gRYdF wr2S9EUAb80kEQ1v0pt9417NOGc1pmYjKCZmDZ7qeGCGk2PR0U59+xJetXBWWhbq 5JxcwdRYoHyrmC/LINxzzqYOQbQevbW0zcEskeKfJsOtj9WJt6U9B1YZbE8pu2QV xjvb+YekD2R+n/umV6eiaGfDau+EWudYVTqY0mR7y9hTiFR/KnqSsy2BUjljpacS XBQO4ig7vY8+1+L3w2xpTN95/rXAvB4BbO/DLea9ArikePoSJ+bVTj0YwrKBghep kOvbvVANrpsunlSAcpXm1qkV+G+xPnyJ -----END PRIVATE KEY----- type: kubernetes.io/tls ================================================ FILE: cmd/flux/testdata/create_secret/tls/test-ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ 4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O 1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb OD8EjjCMY69RMO0= -----END CERTIFICATE----- ================================================ FILE: cmd/flux/testdata/create_secret/tls/test-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUT84jeO/ncOrqI+FY05Fzbg8Ed7MwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA4MDgxNDQyMzVaFw0yMjA4 MDgxNDQyMzVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDn/rPsZ74oypiwCzLlx57zplTiCi/WLSF+MmLGuTvM EQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgSj4YKULCMwwOl45hQPdCTEPJvUhCm M+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigxM9mlg58AlBhVJqn8q64wd/kC/W/K JTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0KYQAhKjEto6ZrYGisfwU1gt3l8M7 sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPps+x/rGFplsPMsXEFFiwvR1+FJZwz lg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7C9V0sjb5trT2lEqZlP2dRSJYt7aJ 1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVkPo2IeEXy5xrR7DyonOQ6Yes0KOCm JB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBCOMXpUESuVZIw8YK+Vd6AExGPPwZ4 n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8rgpbJpPnS+VgryQ/raUQjqDzDCuE 9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFGUB/xsdrcO4yXjqHnAe0yLf8epDjC hh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkOLaVVb9ggPV2SNAHbN4A+St/iRYR5 awIDAQABo1MwUTAdBgNVHQ4EFgQUzMaCqVM30EZFfTeNUIJ5fNPAhaQwHwYDVR0j BBgwFoAUzMaCqVM30EZFfTeNUIJ5fNPAhaQwDwYDVR0TAQH/BAUwAwEB/zANBgkq hkiG9w0BAQsFAAOCAgEAVmk1rXtVkYR1Vs2Va/xrUaGXlFznhPU/Fft44kiEkkLp mLVelWyAqvXYioqssZwuZnTjGz0DQPqzJjqwuGy4CHwPLmhCtfHplrbWo8a0ivYC cL20KfZsG941siUh7LGBjTsq6mWBf2ytlFmg/fg93SgmqcEUAUcdps0JpZD8lgWB ZMstfr6E3jaEus3OsvDD6hJNYZ5clJ5+ynLoWZ99A9JC0U46hmIZpRjbdSvasKpD XrXTdpzyL/Do3znXE/yfoHv4//Rj2CpPHJLYRCIzvuf1mo1fWd53FjHvrbUvaHFz CGuZROd4dC4Rx5nZw2ogIYvJ8m6HpIDkL3pBNSQJtIsvAYEQcotJoa5D/e9fu2Wr +og37oCY4OXzViEBQvyxKD4cajNco1fgGKEaFROADwr3JceGI7Anq5W+xdUvAGNM QuGeCueqNyrJ0CbQ1zEhwgpk/VYfB0u9m0bjMellRlKMdojby+FDCJtAJesx9no4 SQXyx+aNHhj3qReysjGNwZvBk1IHL04HAT+ogNiYhTl1J/YON4MB5UN6Y2PxP6uG KvJGPigx4fAwfR/d78o5ngwoH9m+8FUg8+qllJ8XgIbl/VXKTk3G4ceOm4eBmrel DwWuBhELSjtXWPWhMlkiebgejDbAear53Lia2Cc43zx/KuhMHBTlKY/vY4F2YiI= -----END CERTIFICATE----- ================================================ FILE: cmd/flux/testdata/create_secret/tls/test-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDn/rPsZ74oypiw CzLlx57zplTiCi/WLSF+MmLGuTvMEQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgS j4YKULCMwwOl45hQPdCTEPJvUhCmM+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigx M9mlg58AlBhVJqn8q64wd/kC/W/KJTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0 KYQAhKjEto6ZrYGisfwU1gt3l8M7sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPp s+x/rGFplsPMsXEFFiwvR1+FJZwzlg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7 C9V0sjb5trT2lEqZlP2dRSJYt7aJ1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVk Po2IeEXy5xrR7DyonOQ6Yes0KOCmJB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBC OMXpUESuVZIw8YK+Vd6AExGPPwZ4n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8 rgpbJpPnS+VgryQ/raUQjqDzDCuE9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFG UB/xsdrcO4yXjqHnAe0yLf8epDjChh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkO LaVVb9ggPV2SNAHbN4A+St/iRYR5awIDAQABAoICAQCTxuixQ/wbW8IbEWcgeyHD LkaPndGO6jyVeF73GvL+MDRFuj558NvpNLfqzvTWVf9AnQGMd5Xs9oGegRHu7Csp 3ucp+moBYv7DT14+jtXQKOgGJpDqSqfS1RUKb/TBRXNDLGy02UScziWoAdE33zmf UraVNwW8z1crxKA3yVw2Na++UqhGQlVLAbfXucqnJLVtNWKpkVQlezUgcfmFovsm Iut+9MjI6/sZAqdXTLKuCKo0XjWzNKwnRecE0CYsCwzc80MvFYEiwQi1C0kwoouC iOi8MKM/jDok+5/a3nQ7X+/ho5sbApNCJpfSXAK9YOJ3ju93+RjNuvORfp4/sW3W OGXw6X30Ym7WS/7oYuwEILyqdyNOvKU7a+17d/W/YA60NOdA4iJI3aTfYFMD3l14 Da+D/wkTlEN3Ye7GN21A9AsZwWWiT9G5FOxWWVv7nTPG+Ix5ewehQWt/3DxhSizR inMBizL5xpwx9LRWHnXX277lChYmPFAAMXINl1hnX6s0EY9pSDHN0IddibJkNKBD m1CN37rqxoXQz4zoAyJGfQVkakqe16ayqI9yuQwO6AUkZcD5DYQdz9QYOTnYrQc6 6haC3D0Fmqg1s4v+6gpxZA/qTri0gVl/v/NN4Mk2/qWtK33imOedgD+5LXhZdBgJ Mqn53AErG/AT622jvSb5UQKCAQEA/DTGLh0Ct97PCm+c+PxRFyieaHNJLWENKyxp HoWGHfp2Bvt2Vphoi7GpRCM/yta4vCZgZmeWTQ0yBg6iPVPRA6Ho5hqh9OkUYVoh prL3JsIU20jTutYjo2aefO4qXnJfkkXxNO2FElUHDTwtWdlGJQKvlUJwTv6xO19v bQQkhZSpri6gIpi5Nkm2SGEtDofRJ+F6ThbQibEatL6DR00dh39MYQz+tZP5olzn kX5bHEBWB7gy+YxTGF8FdlCSQTBBtNSKsAv3Cxj4qEHm+fu09vnH6fOZKenT2nXD 5QE/RpgQzLV1TumCjqLzqwp7bbzH+4mjsXpF3KHBZwnhMnDIRwKCAQEA63wYzjBy no0GBBz0hOWrOwQ/AjUHfi47o3Xvl4RBjZclM171HKH7oMCnQvVKTNq8jvakCZjc UI6i+H4R6aokiFS2xGbC2H3ZlSMFNwhb2xUs/C4Nr7JSOWZBtDy5QBspUsp26f7m 9VNVRzCmnxWV9be/1TxHDzDhslNlL5TMejbMorWnrtNG41KWwGtwvv2gApr3894j eJNOh0WGfsMkXUM6+4v4WcCGrdV8Cr6Nvu96ZZe2PWu2dANtAfnxqogXXCoFE6r1 vie7hFSfJ2QR/vEbanED4pYGTtGYP1oseScx0u0hLhGLGccVBUNZlRbox4rIOELI v9MLuiOL4YX7vQKCAQAGzMl3HtMe8AP3DRFXaT4qeK7ktA8KCS7YtibTatg14LXj 9E25gfx3n7+nlae3qVhrwkEhIbPcuflaTnSzYJonFet4oMkzGEGzakG0A+lEA0Ga s/j5daKaWj71sVo1F7JZ+EbLnYfT+bTp93BllsUcZFkllhf/GUDgD++qKc1uSJbW mm044ZNE0nH2u6ACX0kVYS/yAQ14WO0WaHiTqJGeQKFnkHkhni7B4O1hb923AkkP hjjhn5Xx90Xnbb6zwUBURtLCcmAjzXWO29AFd3Lmoc9xEF9V0PckUb6JYyI4ngr9 6fqSuRsLC3u0ZeD0EX322zwtodVWYIodZBfNS1srAoIBAQCjTUPGeUKDQTjS0WGg Z8T/AErRtQSlNFqXWMn2QPlUv2RE460HVi2xpOhZPtFvyqDIY7IOFbtzAfdya7rw V9VN1bGJMdodV+jzy31qVJmerGit2SIUnYz30TnvS80L78oQZ+dfDi4MIuYYoFxs JgQAipS1wz9kAXoCuGKLRJ0og6gVjfPjARE/w55XgiqFyEyWgfFBZOMkUsM6e7Rx Y9Jr+puEpeRsGV9MXafPq6WQq3It0a/HmFLG0TlfDX3RzN6mQ12R7hTM8bDQa/6S yorQSVPB1O3kzDVDo4X5KQd+XPfoVhmUYQYdsjmZlMMi6Og0uMFwgp/Epw6S3uO6 WbfhAoIBAQCOp4iIc87GyxWL8u6HrJaqmFlqkfou0hI+y9h6FfzsBYU6y3+gRYdF wr2S9EUAb80kEQ1v0pt9417NOGc1pmYjKCZmDZ7qeGCGk2PR0U59+xJetXBWWhbq 5JxcwdRYoHyrmC/LINxzzqYOQbQevbW0zcEskeKfJsOtj9WJt6U9B1YZbE8pu2QV xjvb+YekD2R+n/umV6eiaGfDau+EWudYVTqY0mR7y9hTiFR/KnqSsy2BUjljpacS XBQO4ig7vY8+1+L3w2xpTN95/rXAvB4BbO/DLea9ArikePoSJ+bVTj0YwrKBghep kOvbvVANrpsunlSAcpXm1qkV+G+xPnyJ -----END PRIVATE KEY----- ================================================ FILE: cmd/flux/testdata/create_source_chart/basic.yaml ================================================ ✚ generating HelmChart source --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo namespace: {{ .fluxns }} spec: chart: podinfo interval: 0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo ================================================ FILE: cmd/flux/testdata/create_source_chart/setup-source.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 1m0s provider: generic type: oci url: oci://ghcr.io/stefanprodan/charts ================================================ FILE: cmd/flux/testdata/create_source_chart/verify_basic.yaml ================================================ ✚ generating HelmChart source --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo namespace: {{ .fluxns }} spec: chart: podinfo interval: 0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo verify: provider: cosign ================================================ FILE: cmd/flux/testdata/create_source_chart/verify_complete.yaml ================================================ ✚ generating HelmChart source --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo namespace: {{ .fluxns }} spec: chart: podinfo interval: 0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo verify: matchOIDCIdentity: - issuer: foo subject: bar provider: cosign ================================================ FILE: cmd/flux/testdata/create_source_git/export.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: default spec: ignore: |- .cosign non-existent-dir/ interval: 1m0s ref: branch: master sparseCheckout: - .cosign - non-existent-dir/ url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-branch-commit.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: branch: main commit: c88a2f41 url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-branch.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: branch: test url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-commit.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: commit: c88a2f41 url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-provider-azure.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s provider: azure ref: branch: test url: https://dev.azure.com/foo/bar/_git/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-provider-generic.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s provider: generic ref: branch: test url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-provider-github.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s provider: github ref: branch: test secretRef: name: appinfo url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-refname.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: name: refs/heads/main url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-semver.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: semver: v1.01 url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/source-git-tag.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 1m0s ref: tag: test url: https://github.com/stefanprodan/podinfo ================================================ FILE: cmd/flux/testdata/create_source_git/success.golden ================================================ ✚ generating GitRepository source ► applying GitRepository source ✔ GitRepository source created ◎ waiting for GitRepository source reconciliation ✔ GitRepository source reconciliation completed ✔ fetched revision: v1 ================================================ FILE: cmd/flux/testdata/create_source_helm/https.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 5m0s url: https://stefanprodan.github.io/charts/podinfo ================================================ FILE: cmd/flux/testdata/create_source_helm/oci-with-secret.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 5m0s secretRef: name: creds type: oci url: oci://ghcr.io/stefanprodan/charts/podinfo ================================================ FILE: cmd/flux/testdata/create_source_helm/oci.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: podinfo namespace: {{ .fluxns }} spec: interval: 5m0s type: oci url: oci://ghcr.io/stefanprodan/charts/podinfo ================================================ FILE: cmd/flux/testdata/create_tenant/tenant-basic.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: apps --- apiVersion: v1 kind: ServiceAccount metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team namespace: apps --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team-reconciler namespace: apps roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: gotk:apps:reconciler - kind: ServiceAccount name: dev-team namespace: apps ================================================ FILE: cmd/flux/testdata/create_tenant/tenant-with-cluster-role.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: apps --- apiVersion: v1 kind: ServiceAccount metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team namespace: apps --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team-reconciler namespace: apps roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: custom-role subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: gotk:apps:reconciler - kind: ServiceAccount name: dev-team namespace: apps ================================================ FILE: cmd/flux/testdata/create_tenant/tenant-with-service-account.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: apps --- apiVersion: v1 kind: ServiceAccount metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: flux-tenant namespace: apps --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team-reconciler namespace: apps roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: gotk:apps:reconciler - kind: ServiceAccount name: flux-tenant namespace: apps ================================================ FILE: cmd/flux/testdata/create_tenant/tenant-with-skip-namespace.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team namespace: apps --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: toolkit.fluxcd.io/tenant: dev-team name: dev-team-reconciler namespace: apps roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: gotk:apps:reconciler - kind: ServiceAccount name: dev-team namespace: apps ================================================ FILE: cmd/flux/testdata/debug_helmrelease/history-empty.golden.yaml ================================================ # History documentation: https://fluxcd.io/flux/components/helm/helmreleases/#history [] ================================================ FILE: cmd/flux/testdata/debug_helmrelease/history.golden.yaml ================================================ # History documentation: https://fluxcd.io/flux/components/helm/helmreleases/#history - appVersion: 6.0.0 chartName: podinfo chartVersion: 6.0.0 configDigest: sha256:abc123 deleted: "2024-01-01T10:00:00Z" digest: sha256:def456 firstDeployed: "2024-01-01T09:00:00Z" lastDeployed: "2024-01-01T10:00:00Z" name: test-with-history namespace: {{ .fluxns }} status: superseded version: 1 - appVersion: 6.1.0 chartName: podinfo chartVersion: 6.1.0 configDigest: sha256:xyz789 deleted: null digest: sha256:ghi012 firstDeployed: "2024-01-01T11:00:00Z" lastDeployed: "2024-01-01T11:00:00Z" name: test-with-history namespace: {{ .fluxns }} status: deployed version: 2 ================================================ FILE: cmd/flux/testdata/debug_helmrelease/objects.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: test-values-inline namespace: {{ .fluxns }} spec: chartRef: kind: OCIRepository name: podinfo interval: 5m0s values: image: repository: stefanprodan/podinfo tag: 5.0.0 --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: test-values-from namespace: {{ .fluxns }} spec: chartRef: kind: OCIRepository name: podinfo interval: 5m0s values: image: repository: stefanprodan/podinfo tag: 5.0.0 valuesFrom: - kind: ConfigMap name: test - kind: Secret name: test valuesKey: secrets.yaml - kind: Secret name: test valuesKey: flatValue targetPath: aFlatValue - kind: ConfigMap name: none optional: true --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: test-with-history namespace: {{ .fluxns }} spec: chartRef: kind: OCIRepository name: podinfo interval: 5m0s values: image: repository: stefanprodan/podinfo tag: 5.0.0 status: observedGeneration: 1 history: - name: test-with-history namespace: {{ .fluxns }} version: 1 configDigest: sha256:abc123 chartName: podinfo chartVersion: 6.0.0 appVersion: 6.0.0 deleted: "2024-01-01T10:00:00Z" digest: sha256:def456 firstDeployed: "2024-01-01T09:00:00Z" lastDeployed: "2024-01-01T10:00:00Z" status: superseded - name: test-with-history namespace: {{ .fluxns }} version: 2 configDigest: sha256:xyz789 chartName: podinfo chartVersion: 6.1.0 appVersion: 6.1.0 digest: sha256:ghi012 firstDeployed: "2024-01-01T11:00:00Z" lastDeployed: "2024-01-01T11:00:00Z" status: deployed --- apiVersion: v1 kind: ConfigMap metadata: name: test namespace: {{ .fluxns }} data: values.yaml: | cm: "test" override: "cm" --- apiVersion: v1 kind: Secret metadata: name: test namespace: {{ .fluxns }} stringData: secrets.yaml: | secret: "test" override: "secret" flatValue: some-flat-value ================================================ FILE: cmd/flux/testdata/debug_helmrelease/status.golden.yaml ================================================ # Status documentation: https://fluxcd.io/flux/components/helm/helmreleases/#helmrelease-status observedGeneration: -1 ================================================ FILE: cmd/flux/testdata/debug_helmrelease/values-from.golden.yaml ================================================ aFlatValue: some-flat-value cm: test image: repository: stefanprodan/podinfo tag: 5.0.0 override: secret secret: test ================================================ FILE: cmd/flux/testdata/debug_helmrelease/values-inline.golden.yaml ================================================ image: repository: stefanprodan/podinfo tag: 5.0.0 ================================================ FILE: cmd/flux/testdata/debug_kustomization/history-empty.golden.yaml ================================================ # History documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#history [] ================================================ FILE: cmd/flux/testdata/debug_kustomization/history.golden.yaml ================================================ # History documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#history - digest: sha256:def456 firstReconciled: "2024-01-01T09:00:00Z" lastReconciled: "2024-01-01T10:00:00Z" lastReconciledDuration: 300ms lastReconciledStatus: success metadata: originRevision: abc123 totalReconciliations: 1 - digest: sha256:ghi012 firstReconciled: "2024-02-01T09:00:00Z" lastReconciled: "2024-02-01T10:00:00Z" lastReconciledDuration: 500ms lastReconciledStatus: failure metadata: originRevision: xyz789 totalReconciliations: 10 ================================================ FILE: cmd/flux/testdata/debug_kustomization/objects.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: test namespace: {{ .fluxns }} spec: sourceRef: kind: GitRepository name: test interval: 1m path: "./" prune: true postBuild: substitute: TEST_OVERRIDE: "in-line" TEST_INLINE: "in-line" substituteFrom: - kind: ConfigMap name: test - kind: Secret name: test --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: test-from namespace: {{ .fluxns }} spec: sourceRef: kind: GitRepository name: test interval: 1m path: "./" prune: true postBuild: substituteFrom: - kind: ConfigMap name: test - kind: Secret name: test --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: test-with-history namespace: {{ .fluxns }} spec: sourceRef: kind: GitRepository name: test interval: 1m path: "./" prune: true postBuild: substitute: TEST_OVERRIDE: "in-line" TEST_INLINE: "in-line" substituteFrom: - kind: ConfigMap name: test - kind: Secret name: test status: observedGeneration: 1 history: - digest: sha256:def456 firstReconciled: "2024-01-01T09:00:00Z" lastReconciled: "2024-01-01T10:00:00Z" lastReconciledDuration: 300ms lastReconciledStatus: success metadata: originRevision: abc123 totalReconciliations: 1 - digest: sha256:ghi012 firstReconciled: "2024-02-01T09:00:00Z" lastReconciled: "2024-02-01T10:00:00Z" lastReconciledDuration: 500ms lastReconciledStatus: failure metadata: originRevision: xyz789 totalReconciliations: 10 --- apiVersion: v1 kind: ConfigMap metadata: name: test namespace: {{ .fluxns }} data: TEST_OVERRIDE: "cm" TEST_CM: "cm" --- apiVersion: v1 kind: Secret metadata: name: test namespace: {{ .fluxns }} stringData: TEST_OVERRIDE: "secret" TEST_SECRET: "secret" ================================================ FILE: cmd/flux/testdata/debug_kustomization/status.golden.yaml ================================================ # Status documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status observedGeneration: -1 ================================================ FILE: cmd/flux/testdata/debug_kustomization/vars-from.golden.env ================================================ TEST_CM=cm TEST_OVERRIDE=secret TEST_SECRET=secret ================================================ FILE: cmd/flux/testdata/debug_kustomization/vars.golden.env ================================================ TEST_CM=cm TEST_INLINE=in-line TEST_OVERRIDE=in-line TEST_SECRET=secret ================================================ FILE: cmd/flux/testdata/diff-artifact/deployment-diff.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-diff namespace: default spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/diff-artifact/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/diff-artifact/success.golden ================================================ ✔ no changes detected ================================================ FILE: cmd/flux/testdata/diff-kustomization/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 strategy: rollingUpdate: maxUnavailable: 0 type: RollingUpdate selector: matchLabels: app: podinfo template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9797" labels: app: podinfo spec: containers: - name: podinfod image: ghcr.io/stefanprodan/podinfo:6.0.10 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 protocol: TCP - name: http-metrics containerPort: 9797 protocol: TCP - name: grpc containerPort: 9999 protocol: TCP command: - ./podinfo - --port=9898 - --port-metrics=9797 - --grpc-port=9999 - --grpc-service-name=podinfo - --level=info - --random-delay=false - --random-error=false env: - name: PODINFO_UI_COLOR value: "#34577c" livenessProbe: exec: command: - podcli - check - http - localhost:9898/healthz initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 100m memory: 64Mi ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden ================================================ ► 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden ================================================ ► 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden ================================================ ► 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 - one map entry removed: + one map entry added: drift-key: "*****" token: "*****" ► Secret/default/db-user-pass-bkbd782d2c created ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden ================================================ ► 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 data.password ± value change - *** (before) + *** (after) ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden ================================================ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo drifted spec.ports.http.port ± value change - 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden ================================================ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created ► Secret/default/docker-secret created ► Secret/default/secret-basic-auth-stringdata drifted data - one map entry removed: + one map entry added: username1: "*****" username: "*****" ► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/db-user-pass-bkbd782d2c created ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden ================================================ ► 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/diff-with-recursive.golden ================================================ 📁 Kustomization/default/my-app changed ► ConfigMap/default/my-app created ================================================ FILE: cmd/flux/testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml ================================================ 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/flux-kustomization-multiobj.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: podinfo --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo spec: interval: 5m0s path: ./kustomize force: true prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default ================================================ FILE: cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml ================================================ apiVersion: v1 data: drift-key: bXktc2VjcmV0LXRva2VuCg== kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-token-77t89m9b67 namespace: default type: Opaque ================================================ FILE: cmd/flux/testdata/diff-kustomization/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ./deployment.yaml - ./hpa.yaml - ./service.yaml secretGenerator: - literals: - username=admin - password=1f2d1e2e67df name: secret-basic-auth ================================================ FILE: cmd/flux/testdata/diff-kustomization/my-app.yaml ================================================ --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: my-app namespace: default spec: interval: 5m0s path: ./my-app force: true prune: true sourceRef: kind: GitRepository name: podinfo targetNamespace: default ================================================ FILE: cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden ================================================ ► 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 ================================================ FILE: cmd/flux/testdata/diff-kustomization/secret.yaml ================================================ apiVersion: v1 data: password: cGFzc3dvcmQK username: YWRtaW4= kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: db-user-pass-bkbd782d2c namespace: default type: Opaque ================================================ FILE: cmd/flux/testdata/diff-kustomization/service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: default spec: type: ClusterIP selector: app: podinfo ports: - name: http port: 9899 protocol: TCP targetPort: http - port: 9999 targetPort: grpc protocol: TCP name: grpc ================================================ FILE: cmd/flux/testdata/diff-kustomization/stringdata-sops-secret.yaml ================================================ 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= username1: KipTT1BTKio= type: kubernetes.io/basic-auth ================================================ FILE: cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml ================================================ apiVersion: v1 data: token: ZHJpZnQtdmFsdWUK kind: Secret metadata: labels: kustomize.toolkit.fluxcd.io/name: podinfo kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-token-77t89m9b67 namespace: default type: Opaque ================================================ FILE: cmd/flux/testdata/envsubst/file.gold ================================================ apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: test namespace: flux-system spec: ref: branch: main interval: 5m url: ssh://git@github.com/example/test ================================================ FILE: cmd/flux/testdata/envsubst/file.yaml ================================================ apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: ${REPO_NAME} namespace: ${REPO_NAMESPACE:=flux-system} spec: ref: branch: main interval: 5m url: ssh://git@github.com/example/${REPO_NAME} ================================================ FILE: cmd/flux/testdata/export/alert.yaml ================================================ --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: flux-system namespace: {{ .fluxns }} spec: eventSeverity: info eventSources: - kind: GitRepository name: '*' - kind: Kustomization name: '*' providerRef: name: slack summary: Slacktest Notification ================================================ FILE: cmd/flux/testdata/export/bucket.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: Bucket metadata: name: flux-system namespace: {{ .fluxns }} spec: bucketName: podinfo endpoint: s3.amazonaws.com interval: 5m0s provider: aws region: us-east-1 timeout: 30s ================================================ FILE: cmd/flux/testdata/export/external-artifact.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: ExternalArtifact metadata: name: flux-system namespace: {{ .fluxns }} spec: sourceRef: apiVersion: source.example.com/v1alpha1 kind: GitHubRelease name: flux-system namespace: {{ .fluxns }} ================================================ FILE: cmd/flux/testdata/export/git-repo.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m0s ref: branch: main secretRef: name: flux-system timeout: 1m0s url: ssh://git@github.com/example/repo ================================================ FILE: cmd/flux/testdata/export/helm-chart.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: flux-system namespace: {{ .fluxns }} spec: chart: podinfo interval: 1m0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo version: '*' ================================================ FILE: cmd/flux/testdata/export/helm-release.yaml ================================================ --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: flux-system namespace: {{ .fluxns }} spec: chart: spec: chart: podinfo reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: flux-systen namespace: {{ .fluxns }} version: '*' interval: 5m0s ================================================ FILE: cmd/flux/testdata/export/helm-repo.yaml ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m0s provider: generic timeout: 1m0s url: https://stefanprodan.github.io/podinfo ================================================ FILE: cmd/flux/testdata/export/image-policy.yaml ================================================ --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy metadata: name: flux-system namespace: {{ .fluxns }} spec: digestReflectionPolicy: Never imageRepositoryRef: name: flux-system policy: semver: range: 5.0.x ================================================ FILE: cmd/flux/testdata/export/image-repo.yaml ================================================ --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: exclusionList: - ^.*\.sig$ image: ghcr.io/test/podinfo interval: 1m0s provider: generic ================================================ FILE: cmd/flux/testdata/export/image-update.yaml ================================================ --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageUpdateAutomation metadata: name: flux-system namespace: {{ .fluxns }} spec: git: commit: author: email: fluxcdbot@users.noreply.github.com name: fluxcdbot interval: 1m0s sourceRef: kind: GitRepository name: flux-system update: path: ./clusters/my-cluster strategy: Setters ================================================ FILE: cmd/flux/testdata/export/ks.yaml ================================================ --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m0s path: ./infrastructure/ prune: true sourceRef: kind: GitRepository name: flux-system ================================================ FILE: cmd/flux/testdata/export/objects.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: slack namespace: {{ .fluxns }} spec: type: slack channel: 'A channel with spacess' address: https://hooks.slack.com/services/mock --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: flux-system namespace: {{ .fluxns }} spec: summary: "Slacktest Notification" providerRef: name: slack eventSeverity: info eventSources: - kind: "GitRepository" name: "*" - kind: "Kustomization" name: "*" --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: image: ghcr.io/test/podinfo interval: 1m0s --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy metadata: name: flux-system namespace: {{ .fluxns }} spec: imageRepositoryRef: name: flux-system policy: semver: range: 5.0.x --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageUpdateAutomation metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 1m0s sourceRef: kind: GitRepository name: flux-system git: commit: author: email: fluxcdbot@users.noreply.github.com name: fluxcdbot messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}' update: path: ./clusters/my-cluster strategy: Setters --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system interval: 5m url: ssh://git@github.com/example/repo --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system namespace: {{ .fluxns }} spec: path: ./infrastructure/ sourceRef: kind: GitRepository name: flux-system interval: 5m prune: true --- apiVersion: notification.toolkit.fluxcd.io/v1 kind: Receiver metadata: name: flux-system namespace: {{ .fluxns }} spec: type: github events: - "ping" - "push" secretRef: name: webhook-token resources: - kind: GitRepository name: flux-system namespace: flux-system --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m timeout: 1m0s url: https://stefanprodan.github.io/podinfo --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: flux-system namespace: {{ .fluxns }} spec: chart: podinfo interval: 1m0s reconcileStrategy: ChartVersion sourceRef: kind: HelmRepository name: podinfo version: '*' --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m chart: spec: chart: podinfo sourceRef: kind: HelmRepository name: flux-systen namespace: {{ .fluxns }} --- apiVersion: source.toolkit.fluxcd.io/v1 kind: Bucket metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m provider: aws bucketName: podinfo endpoint: s3.amazonaws.com region: us-east-1 timeout: 30s --- apiVersion: source.toolkit.fluxcd.io/v1 kind: ExternalArtifact metadata: name: flux-system namespace: {{ .fluxns }} spec: sourceRef: apiVersion: source.example.com/v1alpha1 kind: GitHubRelease name: flux-system namespace: {{ .fluxns }} ================================================ FILE: cmd/flux/testdata/export/provider.yaml ================================================ --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: slack namespace: {{ .fluxns }} spec: address: https://hooks.slack.com/services/mock channel: A channel with spacess type: slack ================================================ FILE: cmd/flux/testdata/export/receiver.yaml ================================================ --- apiVersion: notification.toolkit.fluxcd.io/v1 kind: Receiver metadata: name: flux-system namespace: {{ .fluxns }} spec: events: - ping - push interval: 10m0s resources: - kind: GitRepository name: flux-system namespace: flux-system secretRef: name: webhook-token type: github ================================================ FILE: cmd/flux/testdata/get/get.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE podinfo main@sha1:696f056d False True Fetched revision: main@sha1:696f056d podinfo-shard1 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d podinfo-shard2 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d ================================================ FILE: cmd/flux/testdata/get/get_label_one.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE podinfo-shard1 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d ================================================ FILE: cmd/flux/testdata/get/get_label_two.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE podinfo main@sha1:696f056d False True Fetched revision: main@sha1:696f056d ================================================ FILE: cmd/flux/testdata/get/objects.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system url: ssh://git@github.com/example/repo interval: 5m status: artifact: lastUpdateTime: "2021-08-01T04:28:42Z" revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f path: "example" url: "example" digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: GitOperationSucceed status: "True" type: Ready --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} sharding.fluxcd.io/key: shard1 name: podinfo-shard1 namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system url: ssh://git@github.com/example/repo interval: 5m status: artifact: lastUpdateTime: "2021-08-01T04:28:42Z" revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f path: "example" url: "example" digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: GitOperationSucceed status: "True" type: Ready --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} sharding.fluxcd.io/key: shard2 name: podinfo-shard2 namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system url: ssh://git@github.com/example/repo interval: 5m status: artifact: lastUpdateTime: "2021-08-01T04:28:42Z" revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f path: "example" url: "example" digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: GitOperationSucceed status: "True" type: Ready ================================================ FILE: cmd/flux/testdata/helmrelease/create_helmrelease_from_git.golden ================================================ ✚ generating HelmRelease ► applying HelmRelease ✔ HelmRelease created ◎ waiting for HelmRelease reconciliation ✔ HelmRelease thrfg is ready ✔ applied revision 6.3.5 ================================================ FILE: cmd/flux/testdata/helmrelease/create_source_git.golden ================================================ ✚ generating GitRepository source ► applying GitRepository source ✔ GitRepository source created ◎ waiting for GitRepository source reconciliation ✔ GitRepository source reconciliation completed ✔ fetched revision: 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/helmrelease/delete_helmrelease_from_git.golden ================================================ ► deleting helmrelease thrfg in {{ .ns }} namespace ✔ helmrelease deleted ================================================ FILE: cmd/flux/testdata/helmrelease/get_helmrelease_from_git.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE thrfg 6.3.5 False True Helm install succeeded for release thrfg-1/thrfg.v1 with chart podinfo@6.3.5 ================================================ FILE: cmd/flux/testdata/helmrelease/reconcile_helmrelease_from_git.golden ================================================ ► annotating GitRepository thrfg in {{ .ns }} namespace ✔ GitRepository annotated ◎ waiting for GitRepository reconciliation ✔ fetched revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ► annotating HelmChart {{ .ns }}-thrfg in {{ .ns }} namespace ✔ HelmChart annotated ◎ waiting for HelmChart reconciliation ✔ fetched revision 6.3.5 ► annotating HelmRelease thrfg in {{ .ns }} namespace ✔ HelmRelease annotated ◎ waiting for HelmRelease reconciliation ✔ applied revision 6.3.5 ================================================ FILE: cmd/flux/testdata/helmrelease/resume_helmrelease_from_git.golden ================================================ ► resuming helmrelease thrfg in {{ .ns }} namespace ✔ helmrelease resumed ◎ waiting for HelmRelease reconciliation ✔ HelmRelease thrfg reconciliation completed ✔ applied revision 6.3.5 ================================================ FILE: cmd/flux/testdata/helmrelease/suspend_helmrelease_from_git.golden ================================================ ► suspending helmrelease thrfg in {{ .ns }} namespace ✔ helmrelease suspended ================================================ FILE: cmd/flux/testdata/image/create_image_policy.golden ================================================ ✚ generating ImagePolicy ► applying ImagePolicy ✔ ImagePolicy created ◎ waiting for ImagePolicy reconciliation ✔ ImagePolicy reconciliation completed ================================================ FILE: cmd/flux/testdata/image/create_image_repository.golden ================================================ ✚ generating ImageRepository ► applying ImageRepository ✔ ImageRepository created ◎ waiting for ImageRepository reconciliation ✔ ImageRepository reconciliation completed ================================================ FILE: cmd/flux/testdata/image/get_image_policy_regex.golden ================================================ NAME IMAGE TAG READY MESSAGE podinfo-regex ghcr.io/stefanprodan/podinfo 5.0.0 True Latest image tag for ghcr.io/stefanprodan/podinfo resolved to 5.0.0 ================================================ FILE: cmd/flux/testdata/image/get_image_policy_semver.golden ================================================ NAME IMAGE TAG READY MESSAGE podinfo-semver ghcr.io/stefanprodan/podinfo 5.0.3 True Latest image tag for ghcr.io/stefanprodan/podinfo resolved to 5.0.3 with digest sha256:8704da90172710d422af855049175c1a8295731cbe2ad3b9a1c1074feecf8c10 ================================================ FILE: cmd/flux/testdata/image/reconcile_image_policy.golden ================================================ ► annotating ImagePolicy podinfo-semver in tis-2 namespace ✔ ImagePolicy annotated ◎ waiting for ImagePolicy reconciliation ✔ selected ref ghcr.io/stefanprodan/podinfo:5.0.3@sha256:8704da90172710d422af855049175c1a8295731cbe2ad3b9a1c1074feecf8c10 ================================================ FILE: cmd/flux/testdata/image/resume_image_policy.golden ================================================ ► resuming image policy podinfo-semver in tis-2 namespace ✔ image policy resumed ◎ waiting for ImagePolicy reconciliation ✔ ImagePolicy podinfo-semver reconciliation completed ✔ selected ref ghcr.io/stefanprodan/podinfo:5.0.3@sha256:8704da90172710d422af855049175c1a8295731cbe2ad3b9a1c1074feecf8c10 ================================================ FILE: cmd/flux/testdata/image/suspend_image_policy.golden ================================================ ► suspending image policy podinfo-semver in tis-2 namespace ✔ image policy suspended ================================================ FILE: cmd/flux/testdata/kustomization/create_kustomization_from_git.golden ================================================ ✚ generating Kustomization ► applying Kustomization ✔ Kustomization created ◎ waiting for Kustomization reconciliation ✔ Kustomization tkfg is ready ✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/kustomization/create_source_git.golden ================================================ ✚ generating GitRepository source ► applying GitRepository source ✔ GitRepository source created ◎ waiting for GitRepository source reconciliation ✔ GitRepository source reconciliation completed ✔ fetched revision: 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/kustomization/delete_kustomization_from_git.golden ================================================ ► deleting kustomization tkfg in {{ .ns }} namespace ✔ kustomization deleted ================================================ FILE: cmd/flux/testdata/kustomization/get_kustomization_from_git.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE tkfg 6.3.5@sha1:67e2c98a False True Applied revision: 6.3.5@sha1:67e2c98a ================================================ FILE: cmd/flux/testdata/kustomization/reconcile_kustomization_from_git.golden ================================================ ► annotating GitRepository tkfg in {{ .ns }} namespace ✔ GitRepository annotated ◎ waiting for GitRepository reconciliation ✔ fetched revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ► annotating Kustomization tkfg in {{ .ns }} namespace ✔ Kustomization annotated ◎ waiting for Kustomization reconciliation ✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/kustomization/resume_kustomization_from_git.golden ================================================ ► resuming kustomization tkfg in {{ .ns }} namespace ✔ kustomization resumed ◎ waiting for Kustomization reconciliation ✔ Kustomization tkfg reconciliation completed ✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args.golden ================================================ ► resuming kustomization tkfg in {{ .ns }} namespace ✔ kustomization resumed ================================================ FILE: cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden ================================================ ► resuming kustomization tkfg in {{ .ns }} namespace ✔ kustomization resumed ✗ Kustomization object 'tkfg' not found in {{ .ns }} namespace ◎ waiting for Kustomization reconciliation ✔ Kustomization tkfg reconciliation completed ✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ================================================ FILE: cmd/flux/testdata/kustomization/suspend_kustomization_from_git.golden ================================================ ► suspending kustomization tkfg in {{ .ns }} namespace ✔ kustomization suspended ================================================ FILE: cmd/flux/testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden ================================================ ► suspending kustomization tkfg in {{ .ns }} namespace ✔ kustomization suspended ✗ Kustomization foo not found in {{ .ns }} namespace ✗ Kustomization bar not found in {{ .ns }} namespace ================================================ FILE: cmd/flux/testdata/logs/all-logs.txt ================================================ 2022-08-02T12:55:34.419Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision 2022-08-02T12:56:04.679Z error GitRepository/flux-system.flux-system - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z info Kustomization/podinfo.default - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision ================================================ FILE: cmd/flux/testdata/logs/kind.txt ================================================ 2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision ================================================ FILE: cmd/flux/testdata/logs/log-level.txt ================================================ 2022-08-02T12:56:04.679Z error GitRepository/flux-system.flux-system - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision ================================================ FILE: cmd/flux/testdata/logs/multiple-filters.txt ================================================ 2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision ================================================ FILE: cmd/flux/testdata/logs/namespace.txt ================================================ 2022-08-02T12:55:34.419Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z info Kustomization/podinfo.default - no changes since last reconcilation: observed revision 2022-08-02T12:56:34.961Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-dir/another-file ================================================ ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-dir/another-file.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation --- ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-dir/another-file.yml ================================================ # This file has Windows line endings. apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation --- ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-file ================================================ ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-file.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy --- spec: wait: true dependsOn: - apiVersion: image.toolkit.fluxcd.io/v1beta1 # update this from v1beta1 kind: ImageRepository # there can be comments here too --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir/some-file.yml ================================================ # This file has Windows line endings. apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImagePolicy --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file ================================================ ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageUpdateAutomation --- ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-dir/another-file.yml ================================================ # This file has Windows line endings. apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageUpdateAutomation --- ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-file ================================================ ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-file.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy --- spec: wait: true dependsOn: - apiVersion: image.toolkit.fluxcd.io/v1 # update this from v1beta1 kind: ImageRepository # there can be comments here too --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.golden/some-file.yml ================================================ # This file has Windows line endings. apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/dir.output.golden ================================================ ► starting migration of custom resources ⚠️ skipping irregular file testdata/migrate/file-system/dir/some-dir/another-file-link.yaml ⚠️ skipping irregular file testdata/migrate/file-system/dir/some-file-link.yaml ✚ testdata/migrate/file-system/dir/some-dir/another-file.yaml:2: ImageUpdateAutomation v1beta2 -> v1 ✚ testdata/migrate/file-system/dir/some-dir/another-file.yml:3: ImageUpdateAutomation v1beta2 -> v1 ⚠️ testdata/migrate/file-system/dir/some-file.yaml:20: unexpected GroupVersion string: image.toolkit.fluxcd.io/v1/v2 ✚ testdata/migrate/file-system/dir/some-file.yaml:1: ImageRepository v1beta1 -> v1 ✚ testdata/migrate/file-system/dir/some-file.yaml:7: ImagePolicy v1beta2 -> v1 ✚ testdata/migrate/file-system/dir/some-file.yaml:14: ImageRepository v1beta1 -> v1 ⚠️ testdata/migrate/file-system/dir/some-file.yml:15: unexpected GroupVersion string: image.toolkit.fluxcd.io/v1/v2 ✚ testdata/migrate/file-system/dir/some-file.yml:3: ImageRepository v1beta2 -> v1 ✚ testdata/migrate/file-system/dir/some-file.yml:9: ImagePolicy v1beta1 -> v1 ✔ file testdata/migrate/file-system/dir/some-dir/another-file.yaml migrated successfully ✔ file testdata/migrate/file-system/dir/some-dir/another-file.yml migrated successfully ✔ file testdata/migrate/file-system/dir/some-file.yaml migrated successfully ✔ file testdata/migrate/file-system/dir/some-file.yml migrated successfully ✔ custom resources migrated successfully ================================================ FILE: cmd/flux/testdata/migrate/file-system/single-file-wrong-ext.json ================================================ ================================================ FILE: cmd/flux/testdata/migrate/file-system/single-file.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/single-file.yaml.golden ================================================ apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy --- apiVersion: image.toolkit.fluxcd.io/v1/v2 kind: ImagePolicy ================================================ FILE: cmd/flux/testdata/migrate/file-system/single-file.yaml.output.golden ================================================ ► starting migration of custom resources ⚠️ testdata/migrate/file-system/single-file.yaml:13: unexpected GroupVersion string: image.toolkit.fluxcd.io/v1/v2 ✚ testdata/migrate/file-system/single-file.yaml:1: ImageRepository v1beta1 -> v1 ✚ testdata/migrate/file-system/single-file.yaml:7: ImagePolicy v1beta2 -> v1 ✔ file testdata/migrate/file-system/single-file.yaml migrated successfully ✔ custom resources migrated successfully ================================================ FILE: cmd/flux/testdata/oci/create_source_oci.golden ================================================ ► applying OCIRepository ✔ OCIRepository created ◎ waiting for OCIRepository reconciliation ✔ OCIRepository reconciliation completed ✔ fetched revision: 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 ================================================ FILE: cmd/flux/testdata/oci/delete_oci.golden ================================================ ► deleting source oci thrfg in {{ .ns }} namespace ✔ source oci deleted ================================================ FILE: cmd/flux/testdata/oci/export.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 10m0s ref: tag: 6.3.5 url: oci://ghcr.io/stefanprodan/manifests/podinfo ================================================ FILE: cmd/flux/testdata/oci/export_with_complete_verification.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 0s ref: tag: 6.3.5 url: oci://ghcr.io/stefanprodan/manifests/podinfo verify: matchOIDCIdentity: - issuer: github subject: stefanprodan provider: cosign ================================================ FILE: cmd/flux/testdata/oci/export_with_issuer.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 0s ref: tag: 6.3.5 url: oci://ghcr.io/stefanprodan/manifests/podinfo verify: matchOIDCIdentity: - issuer: github subject: "" provider: cosign ================================================ FILE: cmd/flux/testdata/oci/export_with_secret.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 10m0s ref: tag: 6.3.5 secretRef: name: creds url: oci://ghcr.io/stefanprodan/manifests/podinfo ================================================ FILE: cmd/flux/testdata/oci/export_with_subject.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 0s ref: tag: 6.3.5 url: oci://ghcr.io/stefanprodan/manifests/podinfo verify: matchOIDCIdentity: - issuer: "" subject: stefanprodan provider: cosign ================================================ FILE: cmd/flux/testdata/oci/export_with_verify_secret.golden ================================================ --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: podinfo namespace: flux-system spec: interval: 10m0s ref: tag: 6.3.5 url: oci://ghcr.io/stefanprodan/manifests/podinfo verify: provider: cosign secretRef: name: cosign-pub ================================================ FILE: cmd/flux/testdata/oci/get_oci.golden ================================================ NAME REVISION SUSPENDED READY MESSAGE thrfg 6.3.5@sha256:6c959c51 False True stored artifact for digest '6.3.5@sha256:6c959c51' ================================================ FILE: cmd/flux/testdata/oci/reconcile_oci.golden ================================================ ► annotating OCIRepository thrfg in {{ .ns }} namespace ✔ OCIRepository annotated ◎ waiting for OCIRepository reconciliation ✔ fetched revision 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 ================================================ FILE: cmd/flux/testdata/oci/resume_oci.golden ================================================ ► resuming source oci thrfg in {{ .ns }} namespace ✔ source oci resumed ◎ waiting for OCIRepository reconciliation ✔ OCIRepository thrfg reconciliation completed ✔ fetched revision 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 ================================================ FILE: cmd/flux/testdata/oci/suspend_oci.golden ================================================ ► suspending source oci thrfg in {{ .ns }} namespace ✔ source oci suspended ================================================ FILE: cmd/flux/testdata/trace/deployment-hr-ocirepo.golden ================================================ Object: deployment/podinfo Namespace: {{ .ns }} Status: Managed by Flux --- HelmRelease: podinfo Namespace: {{ .ns }} Revision: 6.3.5 Status: Last reconciled at {{ .helmReleaseLastReconcile }} Message: Release reconciliation succeeded --- OCIRepository: podinfo-charts Namespace: {{ .fluxns }} URL: oci://ghcr.io/stefanprodan/charts/podinfo Tag: 6.8.0 Revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 Status: Last reconciled at {{ .ociRepositoryLastReconcile }} Message: stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3' ================================================ FILE: cmd/flux/testdata/trace/deployment-hr-ocirepo.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: v1 kind: Namespace metadata: name: {{ .ns }} --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/name: podinfo app.kubernetes.io/managed-by: Helm helm.toolkit.fluxcd.io/name: podinfo helm.toolkit.fluxcd.io/namespace: {{ .ns }} name: podinfo namespace: {{ .ns }} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: podinfo template: metadata: labels: app.kubernetes.io/name: podinfo spec: containers: - name: hello command: [ "echo hello world" ] image: busybox --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: {{ .ns }} spec: chartRef: kind: OCIRepository name: podinfo-charts namespace: {{ .fluxns }} interval: 5m status: conditions: - lastTransitionTime: "2021-07-16T15:42:20Z" message: Release reconciliation succeeded reason: ReconciliationSucceeded status: "True" type: Ready lastAttemptedRevision: 6.3.5 --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-charts namespace: {{ .fluxns }} spec: interval: 10m0s provider: generic ref: tag: 6.8.0 timeout: 60s url: oci://ghcr.io/stefanprodan/charts/podinfo status: artifact: lastUpdateTime: "2022-08-10T10:07:59Z" metadata: org.opencontainers.image.revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git path: "example" revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 url: "example" digest: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: "stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3'" reason: Succeed status: "True" type: Ready ================================================ FILE: cmd/flux/testdata/trace/deployment.golden ================================================ Object: deployment/podinfo Namespace: {{ .ns }} Status: Managed by Flux --- HelmRelease: podinfo Namespace: {{ .ns }} Revision: 6.3.5 Status: Last reconciled at {{ .helmReleaseLastReconcile }} Message: Release reconciliation succeeded --- HelmChart: podinfo-podinfo Namespace: {{ .fluxns }} Chart: podinfo Version: 6.3.5 Revision: 6.3.5 Status: Last reconciled at {{ .helmChartLastReconcile }} Message: Fetched revision: 6.3.5 --- HelmRepository: podinfo Namespace: {{ .fluxns }} URL: https://stefanprodan.github.io/podinfo Revision: sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 Status: Last reconciled at {{ .helmRepositoryLastReconcile }} Message: Fetched revision: main@sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 ================================================ FILE: cmd/flux/testdata/trace/deployment.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: v1 kind: Namespace metadata: name: {{ .ns }} --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/name: podinfo app.kubernetes.io/managed-by: Helm helm.toolkit.fluxcd.io/name: podinfo helm.toolkit.fluxcd.io/namespace: {{ .ns }} name: podinfo namespace: {{ .ns }} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: podinfo template: metadata: labels: app.kubernetes.io/name: podinfo spec: containers: - name: hello command: [ "echo hello world" ] image: busybox --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: labels: kustomize.toolkit.fluxcd.io/name: infrastructure kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: {{ .ns }} spec: chart: spec: chart: podinfo sourceRef: kind: HelmRepository name: podinfo namespace: {{ .fluxns }} interval: 5m status: conditions: - lastTransitionTime: "2021-07-16T15:42:20Z" message: Release reconciliation succeeded reason: ReconciliationSucceeded status: "True" type: Ready helmChart: {{ .fluxns }}/podinfo-podinfo lastAttemptedRevision: 6.3.5 --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo-podinfo namespace: {{ .fluxns }} spec: chart: podinfo sourceRef: kind: HelmRepository name: podinfo version: 6.3.5 interval: 5m status: artifact: checksum: cf13ba96773d9a879cd052c86e73199b3f96c854 lastUpdateTime: "2021-08-01T04:42:55Z" revision: 6.3.5 path: "example" url: "example" digest: sha1:cf13ba96773d9a879cd052c86e73199b3f96c854 conditions: - lastTransitionTime: "2021-07-16T15:32:09Z" message: 'Fetched revision: 6.3.5' reason: ChartPullSucceeded status: "True" type: Ready --- apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: infrastructure kustomize.toolkit.fluxcd.io/namespace: flux-system name: podinfo namespace: {{ .fluxns }} spec: interval: 5m timeout: 1m0s url: https://stefanprodan.github.io/podinfo status: artifact: checksum: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 lastUpdateTime: "2021-07-11T00:25:46Z" revision: sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 path: "example" url: "example" digest: sha256:f105fc5f3b58605631dc25497773d0a392b807cb220f32635fada3ce0dd81ad6 conditions: - lastTransitionTime: "2021-07-11T00:25:46Z" message: 'Fetched revision: main@sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56' reason: IndexationSucceed status: "True" type: Ready --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure namespace: {{ .fluxns }} spec: path: ./infrastructure/ sourceRef: kind: GitRepository name: flux-system interval: 5m prune: true status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: flux-system namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system interval: 5m url: ssh://git@github.com/example/repo ================================================ FILE: cmd/flux/testdata/trace/helmrelease-oci.golden ================================================ Object: HelmRelease/podinfo Namespace: {{ .ns }} Status: Managed by Flux --- Kustomization: infrastructure Namespace: {{ .fluxns }} Path: ./infrastructure Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f Status: Last reconciled at {{ .kustomizationLastReconcile }} Message: Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- OCIRepository: flux-system Namespace: {{ .fluxns }} URL: oci://ghcr.io/example/repo Tag: 1.2.3 Revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 Origin Revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 Origin Source: https://github.com/stefanprodan/podinfo.git Status: Last reconciled at {{ .ociRepositoryLastReconcile }} Message: stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3' ================================================ FILE: cmd/flux/testdata/trace/helmrelease-oci.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: v1 kind: Namespace metadata: name: {{ .ns }} --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: labels: kustomize.toolkit.fluxcd.io/name: infrastructure kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: {{ .ns }} spec: chart: spec: chart: podinfo sourceRef: kind: HelmRepository name: podinfo namespace: {{ .fluxns }} interval: 5m status: conditions: - lastTransitionTime: "2021-07-16T15:42:20Z" message: Release reconciliation succeeded reason: ReconciliationSucceeded status: "True" type: Ready helmChart: {{ .fluxns }}/podinfo-podinfo lastAppliedRevision: 6.3.5 lastAttemptedRevision: 6.3.5 lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531 --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure namespace: {{ .fluxns }} spec: path: ./infrastructure sourceRef: kind: OCIRepository name: flux-system interval: 5m prune: false status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: flux-system namespace: {{ .fluxns }} spec: interval: 10m0s provider: generic ref: tag: 1.2.3 timeout: 60s url: oci://ghcr.io/example/repo status: artifact: lastUpdateTime: "2022-08-10T10:07:59Z" metadata: org.opencontainers.image.revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git path: "example" revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 url: "example" digest: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: "stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3'" reason: Succeed status: "True" type: Ready ================================================ FILE: cmd/flux/testdata/trace/helmrelease.golden ================================================ Object: HelmRelease/podinfo Namespace: {{ .ns }} Status: Managed by Flux --- Kustomization: infrastructure Namespace: {{ .fluxns }} Path: ./infrastructure Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f Status: Last reconciled at {{ .kustomizationLastReconcile }} Message: Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- GitRepository: flux-system Namespace: {{ .fluxns }} URL: ssh://git@github.com/example/repo Branch: main Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f Status: Last reconciled at {{ .gitRepositoryLastReconcile }} Message: Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f ================================================ FILE: cmd/flux/testdata/trace/helmrelease.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: v1 kind: Namespace metadata: name: {{ .ns }} --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: labels: kustomize.toolkit.fluxcd.io/name: infrastructure kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo namespace: {{ .ns }} spec: chart: spec: chart: podinfo sourceRef: kind: HelmRepository name: podinfo namespace: {{ .fluxns }} interval: 5m status: conditions: - lastTransitionTime: "2021-07-16T15:42:20Z" message: Release reconciliation succeeded reason: ReconciliationSucceeded status: "True" type: Ready helmChart: {{ .fluxns }}/podinfo-podinfo lastAppliedRevision: 6.3.5 lastAttemptedRevision: 6.3.5 lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531 --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure namespace: {{ .fluxns }} spec: path: ./infrastructure sourceRef: kind: GitRepository name: flux-system interval: 5m prune: false status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: kustomize.toolkit.fluxcd.io/name: flux-system kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: flux-system namespace: {{ .fluxns }} spec: ref: branch: main secretRef: name: flux-system url: ssh://git@github.com/example/repo interval: 5m status: artifact: lastUpdateTime: "2021-08-01T04:28:42Z" revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f path: "example" url: "example" digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: GitOperationSucceed status: "True" type: Ready ================================================ FILE: cmd/flux/testdata/tree/kustomizations.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: {{ .fluxns }} --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system namespace: {{ .fluxns }} spec: path: ./clusters/production sourceRef: kind: GitRepository name: flux-system interval: 5m prune: true status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready inventory: entries: - id: _{{ .fluxns }}__Namespace v: v1 - id: {{ .fluxns }}_helm-controller_apps_Deployment v: v1 - id: {{ .fluxns }}_kustomize-controller_apps_Deployment v: v1 - id: {{ .fluxns }}_notification-controller_apps_Deployment v: v1 - id: {{ .fluxns }}_source-controller_apps_Deployment v: v1 - id: {{ .fluxns }}_infrastructure_kustomize.toolkit.fluxcd.io_Kustomization v: v1beta2 - id: {{ .fluxns }}_flux-system_source.toolkit.fluxcd.io_GitRepository v: v1beta1 --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure namespace: {{ .fluxns }} spec: path: ./infrastructure/production sourceRef: kind: GitRepository name: flux-system interval: 5m prune: true status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready inventory: entries: - id: _cert-manager__Namespace v: v1 - id: cert-manager_cert-manager_source.toolkit.fluxcd.io_HelmRepository v: v1beta1 --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: empty namespace: {{ .fluxns }} spec: path: ./apps/todo sourceRef: kind: GitRepository name: flux-system interval: 5m prune: true status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready --- ================================================ FILE: cmd/flux/testdata/tree/tree-compact.golden ================================================ Kustomization/{{ .fluxns }}/flux-system ├── Kustomization/{{ .fluxns }}/infrastructure │ └── HelmRepository/cert-manager/cert-manager └── GitRepository/{{ .fluxns }}/flux-system ================================================ FILE: cmd/flux/testdata/tree/tree-empty.golden ================================================ Kustomization/{{ .fluxns }}/empty ================================================ FILE: cmd/flux/testdata/tree/tree.golden ================================================ Kustomization/{{ .fluxns }}/flux-system ├── Namespace/{{ .fluxns }} ├── Deployment/{{ .fluxns }}/helm-controller ├── Deployment/{{ .fluxns }}/kustomize-controller ├── Deployment/{{ .fluxns }}/notification-controller ├── Deployment/{{ .fluxns }}/source-controller ├── Kustomization/{{ .fluxns }}/infrastructure │ ├── Namespace/cert-manager │ └── HelmRepository/cert-manager/cert-manager └── GitRepository/{{ .fluxns }}/flux-system ================================================ FILE: cmd/flux/trace.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "bytes" "context" "fmt" "text/template" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" fluxmeta "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/oci" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var traceCmd = &cobra.Command{ Use: "trace [ ...]", Short: "Trace in-cluster objects throughout the GitOps delivery pipeline", Long: withPreviewNote(`The trace command shows how one or more objects are managed by Flux, from which source and revision they come, and what the latest reconciliation status is. You can also trace multiple objects with different resource kinds using / multiple times.`), Example: ` # Trace a Kubernetes Deployment flux trace -n apps deployment my-app # Trace a Kubernetes Pod and a config map flux trace -n redis pod/redis-master-0 cm/redis # Trace a Kubernetes global object flux trace namespace redis # Trace a Kubernetes custom resource flux trace -n redis helmrelease redis # API Version and Kind can also be specified explicitly # Note that either both, kind and api-version, or neither have to be specified. flux trace redis --kind=helmrelease --api-version=helm.toolkit.fluxcd.io/v2 -n redis`, RunE: traceCmdRun, } type traceFlags struct { apiVersion string kind string } var traceArgs = traceFlags{} func init() { traceCmd.Flags().StringVar(&traceArgs.kind, "kind", "", "the Kubernetes object kind, e.g. Deployment'") traceCmd.Flags().StringVar(&traceArgs.apiVersion, "api-version", "", "the Kubernetes object API version, e.g. 'apps/v1'") rootCmd.AddCommand(traceCmd) } func traceCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var objects []*unstructured.Unstructured if traceArgs.kind != "" || traceArgs.apiVersion != "" { var obj *unstructured.Unstructured obj, err = getObjectStatic(ctx, kubeClient, args) objects = []*unstructured.Unstructured{obj} } else { objects, err = getObjectDynamic(args) } if err != nil { return err } return traceObjects(ctx, kubeClient, objects) } func traceObjects(ctx context.Context, kubeClient client.Client, objects []*unstructured.Unstructured) error { for i, obj := range objects { err := traceObject(ctx, kubeClient, obj) if err != nil { rootCmd.PrintErrf("failed to trace %v/%v in namespace %v: %v", obj.GetKind(), obj.GetName(), obj.GetNamespace(), err) } if i < len(objects)-1 { rootCmd.Println("---") } } return nil } func traceObject(ctx context.Context, kubeClient client.Client, obj *unstructured.Unstructured) error { if ks, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, kustomizev1.GroupVersion.Group); ok { report, err := traceKustomization(ctx, kubeClient, ks, obj) if err != nil { return err } rootCmd.Print(report) return nil } if hr, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, helmv2.GroupVersion.Group); ok { report, err := traceHelm(ctx, kubeClient, hr, obj) if err != nil { return err } rootCmd.Print(report) return nil } return fmt.Errorf("object not managed by Flux") } func getObjectStatic(ctx context.Context, kubeClient client.Client, args []string) (*unstructured.Unstructured, error) { if len(args) < 1 { return nil, fmt.Errorf("object name is required") } if traceArgs.kind == "" { return nil, fmt.Errorf("object kind is required (--kind)") } if traceArgs.apiVersion == "" { return nil, fmt.Errorf("object apiVersion is required (--api-version)") } gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) if err != nil { return nil, fmt.Errorf("invaild apiVersion: %w", err) } obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(schema.GroupVersionKind{ Group: gv.Group, Version: gv.Version, Kind: traceArgs.kind, }) objName := types.NamespacedName{ Namespace: *kubeconfigArgs.Namespace, Name: args[0], } if err = kubeClient.Get(ctx, objName, obj); err != nil { return nil, fmt.Errorf("failed to find object: %w", err) } return obj, nil } func getObjectDynamic(args []string) ([]*unstructured.Unstructured, error) { r := resource.NewBuilder(kubeconfigArgs). Unstructured(). NamespaceParam(*kubeconfigArgs.Namespace).DefaultNamespace(). ResourceTypeOrNameArgs(false, args...). ContinueOnError(). Latest(). Do() if err := r.Err(); err != nil { if resource.IsUsageError(err) { return nil, fmt.Errorf("either `/` or ` ` is required as an argument") } return nil, err } infos, err := r.Infos() if err != nil { return nil, fmt.Errorf("x: %v", err) } if len(infos) == 0 { return nil, fmt.Errorf("failed to find object: %w", err) } objects := []*unstructured.Unstructured{} for _, info := range infos { obj := &unstructured.Unstructured{} obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) if err != nil { return objects, err } objects = append(objects, obj) } return objects, nil } func traceKustomization(ctx context.Context, kubeClient client.Client, ksName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { ks := &kustomizev1.Kustomization{} err := kubeClient.Get(ctx, ksName, ks) if err != nil { return "", fmt.Errorf("failed to find kustomization: %w", err) } ksReady := meta.FindStatusCondition(ks.Status.Conditions, fluxmeta.ReadyCondition) var gitRepository *sourcev1.GitRepository var ociRepository *sourcev1.OCIRepository var externalArtifact *sourcev1.ExternalArtifact var ksRepositoryReady *metav1.Condition switch ks.Spec.SourceRef.Kind { case sourcev1.GitRepositoryKind: gitRepository = &sourcev1.GitRepository{} sourceNamespace := ks.Namespace if ks.Spec.SourceRef.Namespace != "" { sourceNamespace = ks.Spec.SourceRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: ks.Spec.SourceRef.Name, }, gitRepository) if err != nil { return "", fmt.Errorf("failed to find GitRepository: %w", err) } ksRepositoryReady = meta.FindStatusCondition(gitRepository.Status.Conditions, fluxmeta.ReadyCondition) case sourcev1.OCIRepositoryKind: ociRepository = &sourcev1.OCIRepository{} sourceNamespace := ks.Namespace if ks.Spec.SourceRef.Namespace != "" { sourceNamespace = ks.Spec.SourceRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: ks.Spec.SourceRef.Name, }, ociRepository) if err != nil { return "", fmt.Errorf("failed to find OCIRepository: %w", err) } ksRepositoryReady = meta.FindStatusCondition(ociRepository.Status.Conditions, fluxmeta.ReadyCondition) case sourcev1.ExternalArtifactKind: externalArtifact = &sourcev1.ExternalArtifact{} sourceNamespace := ks.Namespace if ks.Spec.SourceRef.Namespace != "" { sourceNamespace = ks.Spec.SourceRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: ks.Spec.SourceRef.Name, }, externalArtifact) if err != nil { return "", fmt.Errorf("failed to find ExternalArtifact: %w", err) } if externalArtifact.Spec.SourceRef == nil { return "", fmt.Errorf("ExternalArtifact %s/%s is missing spec.sourceRef", externalArtifact.Namespace, externalArtifact.Name) } ksRepositoryReady = meta.FindStatusCondition(externalArtifact.Status.Conditions, fluxmeta.ReadyCondition) } var traceTmpl = ` Object: {{.ObjectName}} {{- if .ObjectNamespace }} Namespace: {{.ObjectNamespace}} {{- end }} Status: Managed by Flux {{- if .Kustomization }} --- Kustomization: {{.Kustomization.Name}} Namespace: {{.Kustomization.Namespace}} {{- if .Kustomization.Spec.TargetNamespace }} Target: {{.Kustomization.Spec.TargetNamespace}} {{- end }} Path: {{.Kustomization.Spec.Path}} Revision: {{.Kustomization.Status.LastAppliedRevision}} {{- if .KustomizationReady }} Status: Last reconciled at {{.KustomizationReady.LastTransitionTime}} Message: {{.KustomizationReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .GitRepository }} --- GitRepository: {{.GitRepository.Name}} Namespace: {{.GitRepository.Namespace}} URL: {{.GitRepository.Spec.URL}} {{- if .GitRepository.Spec.Reference }} {{- if .GitRepository.Spec.Reference.Tag }} Tag: {{.GitRepository.Spec.Reference.Tag}} {{- else if .GitRepository.Spec.Reference.SemVer }} Tag: {{.GitRepository.Spec.Reference.SemVer}} {{- else if .GitRepository.Spec.Reference.Branch }} Branch: {{.GitRepository.Spec.Reference.Branch}} {{- end }} {{- end }} {{- if .GitRepository.Status.Artifact }} Revision: {{.GitRepository.Status.Artifact.Revision}} {{- end }} {{- if .RepositoryReady }} {{- if eq .RepositoryReady.Status "False" }} Status: Last reconciliation failed at {{.RepositoryReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.RepositoryReady.LastTransitionTime}} {{- end }} Message: {{.RepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .OCIRepository }} --- OCIRepository: {{.OCIRepository.Name}} Namespace: {{.OCIRepository.Namespace}} URL: {{.OCIRepository.Spec.URL}} {{- if .OCIRepository.Spec.Reference }} {{- if .OCIRepository.Spec.Reference.Tag }} Tag: {{.OCIRepository.Spec.Reference.Tag}} {{- else if .OCIRepository.Spec.Reference.SemVer }} Tag: {{.OCIRepository.Spec.Reference.SemVer}} {{- else if .OCIRepository.Spec.Reference.Digest }} Digest: {{.OCIRepository.Spec.Reference.Digest}} {{- end }} {{- end }} {{- if .OCIRepository.Status.Artifact }} Revision: {{.OCIRepository.Status.Artifact.Revision}} {{- if .OCIRepository.Status.Artifact.Metadata }} {{- $metadata := .OCIRepository.Status.Artifact.Metadata }} {{- range $k, $v := .Annotations }} {{ with (index $metadata $v) }}{{ $k }}{{ . }}{{ end }} {{- end }} {{- end }} {{- end }} {{- if .RepositoryReady }} {{- if eq .RepositoryReady.Status "False" }} Status: Last reconciliation failed at {{.RepositoryReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.RepositoryReady.LastTransitionTime}} {{- end }} Message: {{.RepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .ExternalArtifact }} --- ExternalArtifact:{{.ExternalArtifact.Name}} Namespace: {{.ExternalArtifact.Namespace}} Source: {{.ExternalArtifact.Spec.SourceRef.Kind}}/{{.ExternalArtifact.Spec.SourceRef.Namespace}}/{{.ExternalArtifact.Spec.SourceRef.Name}} {{- if .ExternalArtifact.Status.Artifact }} Revision: {{.ExternalArtifact.Status.Artifact.Revision}} {{- if .ExternalArtifact.Status.Artifact.Metadata }} {{- $metadata := .ExternalArtifact.Status.Artifact.Metadata }} {{- range $k, $v := .Annotations }} {{ with (index $metadata $v) }}{{ $k }}{{ . }}{{ end }} {{- end }} {{- end }} {{- end }} {{- if .RepositoryReady }} {{- if eq .RepositoryReady.Status "False" }} Status: Last reconciliation failed at {{.RepositoryReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.RepositoryReady.LastTransitionTime}} {{- end }} Message: {{.RepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} ` traceResult := struct { ObjectName string ObjectNamespace string Kustomization *kustomizev1.Kustomization KustomizationReady *metav1.Condition GitRepository *sourcev1.GitRepository OCIRepository *sourcev1.OCIRepository ExternalArtifact *sourcev1.ExternalArtifact RepositoryReady *metav1.Condition Annotations map[string]string }{ ObjectName: obj.GetKind() + "/" + obj.GetName(), ObjectNamespace: obj.GetNamespace(), Kustomization: ks, KustomizationReady: ksReady, GitRepository: gitRepository, OCIRepository: ociRepository, ExternalArtifact: externalArtifact, RepositoryReady: ksRepositoryReady, Annotations: map[string]string{"Origin Source: ": oci.SourceAnnotation, "Origin Revision: ": oci.RevisionAnnotation}, } t, err := template.New("tmpl").Parse(traceTmpl) if err != nil { return "", err } var data bytes.Buffer writer := bufio.NewWriter(&data) if err := t.Execute(writer, traceResult); err != nil { return "", err } if err := writer.Flush(); err != nil { return "", err } return data.String(), nil } func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { hr := &helmv2.HelmRelease{} err := kubeClient.Get(ctx, hrName, hr) if err != nil { return "", fmt.Errorf("failed to find HelmRelease: %w", err) } hrReady := meta.FindStatusCondition(hr.Status.Conditions, fluxmeta.ReadyCondition) var hrChart *sourcev1.HelmChart var hrChartReady *metav1.Condition if chart := hr.Status.HelmChart; chart != "" { hrChart = &sourcev1.HelmChart{} err = kubeClient.Get(ctx, utils.ParseNamespacedName(chart), hrChart) if err != nil { return "", fmt.Errorf("failed to find HelmChart: %w", err) } hrChartReady = meta.FindStatusCondition(hrChart.Status.Conditions, fluxmeta.ReadyCondition) } var hrGitRepository *sourcev1.GitRepository var hrGitRepositoryReady *metav1.Condition var hrHelmRepository *sourcev1.HelmRepository var hrHelmRepositoryReady *metav1.Condition var hrOCIRepository *sourcev1.OCIRepository var hrOCIRepositoryReady *metav1.Condition var hrExternalArtifact *sourcev1.ExternalArtifact var hrExternalArtifactReady *metav1.Condition if hr.Spec.Chart == nil { if hr.Spec.ChartRef != nil { switch hr.Spec.ChartRef.Kind { case sourcev1.OCIRepositoryKind: hrOCIRepository = &sourcev1.OCIRepository{} sourceNamespace := hr.Namespace if hr.Spec.ChartRef.Namespace != "" { sourceNamespace = hr.Spec.ChartRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: hr.Spec.ChartRef.Name, }, hrOCIRepository) if err != nil { return "", fmt.Errorf("failed to find OCIRepository: %w", err) } hrOCIRepositoryReady = meta.FindStatusCondition(hrOCIRepository.Status.Conditions, fluxmeta.ReadyCondition) case sourcev1.ExternalArtifactKind: hrExternalArtifact = &sourcev1.ExternalArtifact{} sourceNamespace := hr.Namespace if hr.Spec.ChartRef.Namespace != "" { sourceNamespace = hr.Spec.ChartRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: hr.Spec.ChartRef.Name, }, hrExternalArtifact) if err != nil { return "", fmt.Errorf("failed to find ExternalArtifact: %w", err) } if hrExternalArtifact.Spec.SourceRef == nil { return "", fmt.Errorf("ExternalArtifact %s/%s is missing spec.sourceRef", hrExternalArtifact.Namespace, hrExternalArtifact.Name) } hrExternalArtifactReady = meta.FindStatusCondition(hrExternalArtifact.Status.Conditions, fluxmeta.ReadyCondition) } } } else { if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind { hrGitRepository = &sourcev1.GitRepository{} sourceNamespace := hr.Namespace if hr.Spec.Chart.Spec.SourceRef.Namespace != "" { sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: hr.Spec.Chart.Spec.SourceRef.Name, }, hrGitRepository) if err != nil { return "", fmt.Errorf("failed to find GitRepository: %w", err) } hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition) } if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind { hrHelmRepository = &sourcev1.HelmRepository{} sourceNamespace := hr.Namespace if hr.Spec.Chart.Spec.SourceRef.Namespace != "" { sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace } err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: hr.Spec.Chart.Spec.SourceRef.Name, }, hrHelmRepository) if err != nil { return "", fmt.Errorf("failed to find HelmRepository: %w", err) } hrHelmRepositoryReady = meta.FindStatusCondition(hrHelmRepository.Status.Conditions, fluxmeta.ReadyCondition) } } var traceTmpl = ` Object: {{.ObjectName}} {{- if .ObjectNamespace }} Namespace: {{.ObjectNamespace}} {{- end }} Status: Managed by Flux {{- if .HelmRelease }} --- HelmRelease: {{.HelmRelease.Name}} Namespace: {{.HelmRelease.Namespace}} {{- if .HelmRelease.Spec.TargetNamespace }} Target: {{.HelmRelease.Spec.TargetNamespace}} {{- end }} Revision: {{.HelmRelease.Status.LastAttemptedRevision}} {{- if .HelmReleaseReady }} Status: Last reconciled at {{.HelmReleaseReady.LastTransitionTime}} Message: {{.HelmReleaseReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .HelmChart }} --- HelmChart: {{.HelmChart.Name}} Namespace: {{.HelmChart.Namespace}} Chart: {{.HelmChart.Spec.Chart}} Version: {{.HelmChart.Spec.Version}} {{- if .HelmChart.Status.Artifact }} Revision: {{.HelmChart.Status.Artifact.Revision}} {{- end }} {{- if .HelmChartReady }} Status: Last reconciled at {{.HelmChartReady.LastTransitionTime}} Message: {{.HelmChartReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .HelmRepository }} --- HelmRepository: {{.HelmRepository.Name}} Namespace: {{.HelmRepository.Namespace}} URL: {{.HelmRepository.Spec.URL}} {{- if .HelmRepository.Status.Artifact }} Revision: {{.HelmRepository.Status.Artifact.Revision}} {{- end }} {{- if .HelmRepositoryReady }} Status: Last reconciled at {{.HelmRepositoryReady.LastTransitionTime}} Message: {{.HelmRepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .GitRepository }} --- GitRepository: {{.GitRepository.Name}} Namespace: {{.GitRepository.Namespace}} URL: {{.GitRepository.Spec.URL}} {{- if .GitRepository.Spec.Reference }} {{- if .GitRepository.Spec.Reference.Tag }} Tag: {{.GitRepository.Spec.Reference.Tag}} {{- else if .GitRepository.Spec.Reference.SemVer }} Tag: {{.GitRepository.Spec.Reference.SemVer}} {{- else if .GitRepository.Spec.Reference.Branch }} Branch: {{.GitRepository.Spec.Reference.Branch}} {{- end }} {{- end }} {{- if .GitRepository.Status.Artifact }} Revision: {{.GitRepository.Status.Artifact.Revision}} {{- end }} {{- if .GitRepositoryReady }} {{- if eq .GitRepositoryReady.Status "False" }} Status: Last reconciliation failed at {{.GitRepositoryReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} {{- end }} Message: {{.GitRepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .OCIRepository }} --- OCIRepository: {{.OCIRepository.Name}} Namespace: {{.OCIRepository.Namespace}} URL: {{.OCIRepository.Spec.URL}} {{- if .OCIRepository.Spec.Reference }} {{- if .OCIRepository.Spec.Reference.Tag }} Tag: {{.OCIRepository.Spec.Reference.Tag}} {{- else if .OCIRepository.Spec.Reference.SemVer }} Tag: {{.OCIRepository.Spec.Reference.SemVer}} {{- else if .OCIRepository.Spec.Reference.Digest }} Digest: {{.OCIRepository.Spec.Reference.Digest}} {{- end }} {{- end }} {{- if .OCIRepository.Status.Artifact }} Revision: {{.OCIRepository.Status.Artifact.Revision}} {{- end }} {{- if .OCIRepositoryReady }} {{- if eq .OCIRepositoryReady.Status "False" }} Status: Last reconciliation failed at {{.OCIRepositoryReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.OCIRepositoryReady.LastTransitionTime}} {{- end }} Message: {{.OCIRepositoryReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} {{- if .ExternalArtifact }} --- ExternalArtifact:{{.ExternalArtifact.Name}} Namespace: {{.ExternalArtifact.Namespace}} Source: {{.ExternalArtifact.Spec.SourceRef.Kind}}/{{.ExternalArtifact.Spec.SourceRef.Namespace}}/{{.ExternalArtifact.Spec.SourceRef.Name}} {{- if .ExternalArtifact.Status.Artifact }} Revision: {{.ExternalArtifact.Status.Artifact.Revision}} {{- end }} {{- if .ExternalArtifactReady }} {{- if eq .ExternalArtifactReady.Status "False" }} Status: Last reconciliation failed at {{.ExternalArtifactReady.LastTransitionTime}} {{- else }} Status: Last reconciled at {{.ExternalArtifactReady.LastTransitionTime}} {{- end }} Message: {{.ExternalArtifactReady.Message}} {{- else }} Status: Unknown {{- end }} {{- end }} ` traceResult := struct { ObjectName string ObjectNamespace string HelmRelease *helmv2.HelmRelease HelmReleaseReady *metav1.Condition HelmChart *sourcev1.HelmChart HelmChartReady *metav1.Condition GitRepository *sourcev1.GitRepository GitRepositoryReady *metav1.Condition HelmRepository *sourcev1.HelmRepository HelmRepositoryReady *metav1.Condition OCIRepository *sourcev1.OCIRepository OCIRepositoryReady *metav1.Condition ExternalArtifact *sourcev1.ExternalArtifact ExternalArtifactReady *metav1.Condition Annotations map[string]string }{ ObjectName: obj.GetKind() + "/" + obj.GetName(), ObjectNamespace: obj.GetNamespace(), HelmRelease: hr, HelmReleaseReady: hrReady, HelmChart: hrChart, HelmChartReady: hrChartReady, GitRepository: hrGitRepository, GitRepositoryReady: hrGitRepositoryReady, HelmRepository: hrHelmRepository, HelmRepositoryReady: hrHelmRepositoryReady, OCIRepository: hrOCIRepository, OCIRepositoryReady: hrOCIRepositoryReady, ExternalArtifact: hrExternalArtifact, ExternalArtifactReady: hrExternalArtifactReady, } t, err := template.New("tmpl").Parse(traceTmpl) if err != nil { return "", err } var data bytes.Buffer writer := bufio.NewWriter(&data) if err := t.Execute(writer, traceResult); err != nil { return "", err } if err := writer.Flush(); err != nil { return "", err } return data.String(), nil } func isManagedByFlux(obj *unstructured.Unstructured, group string) (types.NamespacedName, bool) { nameKey := fmt.Sprintf("%s/name", group) namespaceKey := fmt.Sprintf("%s/namespace", group) namespacedName := types.NamespacedName{} for k, v := range obj.GetLabels() { if k == nameKey { namespacedName.Name = v } if k == namespaceKey { namespacedName.Namespace = v } } if namespacedName.Name == "" { return namespacedName, false } return namespacedName, true } func isOwnerManagedByFlux(ctx context.Context, kubeClient client.Client, obj *unstructured.Unstructured, group string) (types.NamespacedName, bool) { if n, ok := isManagedByFlux(obj, group); ok { return n, true } namespacedName := types.NamespacedName{} for _, reference := range obj.GetOwnerReferences() { owner := &unstructured.Unstructured{} gv, err := schema.ParseGroupVersion(reference.APIVersion) if err != nil { return namespacedName, false } owner.SetGroupVersionKind(schema.GroupVersionKind{ Group: gv.Group, Version: gv.Version, Kind: reference.Kind, }) ownerName := types.NamespacedName{ Namespace: obj.GetNamespace(), Name: reference.Name, } err = kubeClient.Get(ctx, ownerName, owner) if err != nil { return namespacedName, false } if n, ok := isManagedByFlux(owner, group); ok { return n, true } if len(owner.GetOwnerReferences()) > 0 { return isOwnerManagedByFlux(ctx, kubeClient, owner, group) } } return namespacedName, false } ================================================ FILE: cmd/flux/trace_test.go ================================================ //go:build unit // +build unit /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" "time" ) func TestTraceNoArgs(t *testing.T) { cmd := cmdTestCase{ args: "trace", assert: assertError("either `/` or ` ` is required as an argument"), } cmd.runTestCmd(t) } func toLocalTime(t *testing.T, in string) string { ts, err := time.Parse(time.RFC3339, in) if err != nil { t.Fatalf("Error converting golden test time '%s': %v", in, err) } return ts.Local().String() } func TestTrace(t *testing.T) { cases := []struct { name string args string objectFile string goldenFile string tmpl map[string]string }{ { "Deployment", "trace podinfo --kind deployment --api-version=apps/v1", "testdata/trace/deployment.yaml", "testdata/trace/deployment.golden", map[string]string{ "ns": allocateNamespace("podinfo"), "fluxns": allocateNamespace("flux-system"), "helmReleaseLastReconcile": toLocalTime(t, "2021-07-16T15:42:20Z"), "helmChartLastReconcile": toLocalTime(t, "2021-07-16T15:32:09Z"), "helmRepositoryLastReconcile": toLocalTime(t, "2021-07-11T00:25:46Z"), }, }, { "HelmRelease", "trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2", "testdata/trace/helmrelease.yaml", "testdata/trace/helmrelease.golden", map[string]string{ "ns": allocateNamespace("podinfo"), "fluxns": allocateNamespace("flux-system"), "kustomizationLastReconcile": toLocalTime(t, "2021-08-01T04:52:56Z"), "gitRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"), }, }, { "HelmRelease from OCI registry", "trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2", "testdata/trace/helmrelease-oci.yaml", "testdata/trace/helmrelease-oci.golden", map[string]string{ "ns": allocateNamespace("podinfo"), "fluxns": allocateNamespace("flux-system"), "kustomizationLastReconcile": toLocalTime(t, "2021-08-01T04:52:56Z"), "ociRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"), }, }, { "Deployment from HelmRelease from OCI registry", "trace podinfo --kind deployment --api-version=apps/v1", "testdata/trace/deployment-hr-ocirepo.yaml", "testdata/trace/deployment-hr-ocirepo.golden", map[string]string{ "ns": allocateNamespace("podinfo"), "fluxns": allocateNamespace("flux-system"), "helmReleaseLastReconcile": toLocalTime(t, "2021-07-16T15:42:20Z"), "ociRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"), }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { testEnv.CreateObjectFile(tc.objectFile, tc.tmpl, t) cmd := cmdTestCase{ args: tc.args + " -n=" + tc.tmpl["ns"], assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/tree.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var treeCmd = &cobra.Command{ Use: "tree", Short: "Print the resources reconciled by Flux", Long: withPreviewNote(`The tree command shows the list of resources reconciled by a Flux object.`), } func init() { rootCmd.AddCommand(treeCmd) } ================================================ FILE: cmd/flux/tree_artifact.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "github.com/spf13/cobra" ) var treeArtifactCmd = &cobra.Command{ Use: "artifact", Short: "Print artifact objects reconciled by Flux", Long: `The tree artifact sub-commands print a list of artifact objects.`, } func init() { treeCmd.AddCommand(treeArtifactCmd) } ================================================ FILE: cmd/flux/tree_artifact_generator.go ================================================ /* Copyright 2025 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/fluxcd/cli-utils/pkg/object" sourcev1 "github.com/fluxcd/source-controller/api/v1" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" "github.com/fluxcd/flux2/v2/internal/tree" "github.com/fluxcd/flux2/v2/internal/utils" ) var treeArtifactGeneratorCmd = &cobra.Command{ Use: "generator [name]", Short: "Print the inventory of an ArtifactGenerator", Long: withPreviewNote(`The tree command prints the ExternalArtifact list managed by an ArtifactGenerator.'`), Example: ` # Print the ExternalArtifacts managed by an ArtifactGenerator flux tree artifact generator my-generator`, RunE: treeArtifactGeneratorCmdRun, Args: cobra.ExactArgs(1), ValidArgsFunction: resourceNamesCompletionFunc(swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)), } type TreeArtifactGeneratorFlags struct { output string } var treeArtifactGeneratorArgs TreeArtifactGeneratorFlags func init() { treeArtifactGeneratorCmd.Flags().StringVarP(&treeArtifactGeneratorArgs.output, "output", "o", "", "the format in which the tree should be printed. can be 'json' or 'yaml'") treeArtifactCmd.AddCommand(treeArtifactGeneratorCmd) } func treeArtifactGeneratorCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("generator name is required") } name := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } ag := &swapi.ArtifactGenerator{} err = kubeClient.Get(ctx, client.ObjectKey{ Namespace: *kubeconfigArgs.Namespace, Name: name, }, ag) if err != nil { return err } kTree := tree.New(object.ObjMetadata{ Namespace: ag.Namespace, Name: ag.Name, GroupKind: schema.GroupKind{Group: swapi.GroupVersion.Group, Kind: swapi.ArtifactGeneratorKind}, }) for _, ea := range ag.Status.Inventory { kTree.Add(object.ObjMetadata{ Namespace: ea.Namespace, Name: ea.Name, GroupKind: schema.GroupKind{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.ExternalArtifactKind}, }) } switch treeArtifactGeneratorArgs.output { case "json": data, err := json.MarshalIndent(kTree, "", " ") if err != nil { return err } rootCmd.Println(string(data)) case "yaml": data, err := yaml.Marshal(kTree) if err != nil { return err } rootCmd.Println(string(data)) default: rootCmd.Println(kTree.Print()) } return nil } ================================================ FILE: cmd/flux/tree_kustomization.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "compress/gzip" "context" "encoding/base64" "encoding/json" "fmt" "io" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/yaml" "github.com/fluxcd/cli-utils/pkg/object" helmv2 "github.com/fluxcd/helm-controller/api/v2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/internal/tree" "github.com/fluxcd/flux2/v2/internal/utils" ) var treeKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks", "kustomization"}, Short: "Print the resource inventory of a Kustomization", Long: withPreviewNote(`The tree command prints the resource list reconciled by a Kustomization.'`), Example: ` # Print the resources managed by the root Kustomization flux tree kustomization flux-system # Print the Flux resources managed by the root Kustomization flux tree kustomization flux-system --compact`, RunE: treeKsCmdRun, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), } type TreeKsFlags struct { compact bool output string } var treeKsArgs TreeKsFlags func init() { treeKsCmd.Flags().BoolVar(&treeKsArgs.compact, "compact", false, "list Flux resources only.") treeKsCmd.Flags().StringVarP(&treeKsArgs.output, "output", "o", "", "the format in which the tree should be printed. can be 'json' or 'yaml'") treeCmd.AddCommand(treeKsCmd) } func treeKsCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("kustomization name is required") } name := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } k := &kustomizev1.Kustomization{} err = kubeClient.Get(ctx, client.ObjectKey{ Namespace: *kubeconfigArgs.Namespace, Name: name, }, k) if err != nil { return err } kTree := tree.New(object.ObjMetadata{ Namespace: k.Namespace, Name: k.Name, GroupKind: schema.GroupKind{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}, }) err = treeKustomization(ctx, kTree, k, kubeClient, treeKsArgs.compact) if err != nil { return err } switch treeKsArgs.output { case "json": data, err := json.MarshalIndent(kTree, "", " ") if err != nil { return err } rootCmd.Println(string(data)) case "yaml": data, err := yaml.Marshal(kTree) if err != nil { return err } rootCmd.Println(string(data)) default: rootCmd.Println(kTree.Print()) } return nil } func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kustomizev1.Kustomization, kubeClient client.Client, compact bool) error { if item.Status.Inventory == nil || len(item.Status.Inventory.Entries) == 0 { return nil } compactGroup := "toolkit.fluxcd.io" for _, entry := range item.Status.Inventory.Entries { objMetadata, err := object.ParseObjMetadata(entry.ID) if err != nil { return err } if compact && !strings.Contains(objMetadata.GroupKind.Group, compactGroup) { continue } if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind && objMetadata.Namespace == item.Namespace && objMetadata.Name == item.Name { continue } ks := tree.Add(objMetadata) if objMetadata.GroupKind.Group == helmv2.GroupVersion.Group && objMetadata.GroupKind.Kind == helmv2.HelmReleaseKind { objects, err := getHelmReleaseInventory( ctx, client.ObjectKey{ Namespace: objMetadata.Namespace, Name: objMetadata.Name, }, kubeClient) if err != nil { return err } for _, obj := range objects { if compact && !strings.Contains(obj.GroupKind.Group, compactGroup) { continue } ks.Add(obj) } } if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind && // skip kustomization if it targets a remote clusters item.Spec.KubeConfig == nil { k := &kustomizev1.Kustomization{} err = kubeClient.Get(ctx, client.ObjectKey{ Namespace: objMetadata.Namespace, Name: objMetadata.Name, }, k) if err != nil { return fmt.Errorf("failed to find object: %w", err) } err := treeKustomization(ctx, ks, k, kubeClient, compact) if err != nil { return err } } } return nil } type hrStorage struct { Name string `json:"name,omitempty"` Manifest string `json:"manifest,omitempty"` } func getHelmReleaseInventory(ctx context.Context, objectKey client.ObjectKey, kubeClient client.Client) ([]object.ObjMetadata, error) { hr := &helmv2.HelmRelease{} if err := kubeClient.Get(ctx, objectKey, hr); err != nil { return nil, err } // skip release if it targets a remote clusters if hr.Spec.KubeConfig != nil { return nil, nil } storageNamespace := hr.Status.StorageNamespace latest := hr.Status.History.Latest() if len(storageNamespace) == 0 || latest == nil { // Skip release if it has no current return nil, nil } storageKey := client.ObjectKey{ Namespace: storageNamespace, Name: fmt.Sprintf("sh.helm.release.v1.%s.v%v", latest.Name, latest.Version), } storageSecret := &corev1.Secret{} if err := kubeClient.Get(ctx, storageKey, storageSecret); err != nil { // skip release if it has no storage if apierrors.IsNotFound(err) { return nil, nil } return nil, fmt.Errorf("failed to find the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) } releaseData, releaseFound := storageSecret.Data["release"] if !releaseFound { return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s'", objectKey.String()) } // adapted from https://github.com/helm/helm/blob/02685e94bd3862afcb44f6cd7716dbeb69743567/pkg/storage/driver/util.go var b64 = base64.StdEncoding b, err := b64.DecodeString(string(releaseData)) if err != nil { return nil, err } var magicGzip = []byte{0x1f, 0x8b, 0x08} if bytes.Equal(b[0:3], magicGzip) { r, err := gzip.NewReader(bytes.NewReader(b)) if err != nil { return nil, err } defer r.Close() b2, err := io.ReadAll(r) if err != nil { return nil, err } b = b2 } // extract objects from Helm storage var rls hrStorage if err := json.Unmarshal(b, &rls); err != nil { return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) } objects, err := ssautil.ReadObjects(strings.NewReader(rls.Manifest)) if err != nil { return nil, fmt.Errorf("failed to read the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) } // set the namespace on namespaced objects for _, obj := range objects { if obj.GetNamespace() == "" { if isNamespaced, _ := apiutil.IsObjectNamespaced(obj, kubeClient.Scheme(), kubeClient.RESTMapper()); isNamespaced { obj.SetNamespace(latest.Namespace) } } } result := object.UnstructuredSetToObjMetadataSet(objects) // search for CRDs managed by the HelmRelease if installing or upgrading CRDs is enabled in spec if (hr.Spec.Install != nil && len(hr.Spec.Install.CRDs) > 0 && hr.Spec.Install.CRDs != helmv2.Skip) || (hr.Spec.Upgrade != nil && len(hr.Spec.Upgrade.CRDs) > 0 && hr.Spec.Upgrade.CRDs != helmv2.Skip) { selector := client.MatchingLabels{ fmt.Sprintf("%s/name", helmv2.GroupVersion.Group): hr.GetName(), fmt.Sprintf("%s/namespace", helmv2.GroupVersion.Group): hr.GetNamespace(), } crdKind := "CustomResourceDefinition" var list apiextensionsv1.CustomResourceDefinitionList if err := kubeClient.List(ctx, &list, selector); err == nil { for _, crd := range list.Items { found := false for _, r := range result { if r.Name == crd.GetName() && r.GroupKind.Kind == crdKind { found = true break } } if !found { result = append(result, object.ObjMetadata{ Name: crd.GetName(), GroupKind: schema.GroupKind{ Group: apiextensionsv1.GroupName, Kind: crdKind, }, }) } } } } return result, nil } ================================================ FILE: cmd/flux/tree_kustomization_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestTree(t *testing.T) { cases := []struct { name string args string objectFile string goldenFile string }{ { "tree kustomization", "tree kustomization flux-system", "testdata/tree/kustomizations.yaml", "testdata/tree/tree.golden", }, { "tree kustomization compact", "tree kustomization flux-system --compact", "testdata/tree/kustomizations.yaml", "testdata/tree/tree-compact.golden", }, { "tree kustomization empty", "tree kustomization empty", "testdata/tree/kustomizations.yaml", "testdata/tree/tree-empty.golden", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } testEnv.CreateObjectFile(tc.objectFile, tmpl, t) cmd := cmdTestCase{ args: tc.args + " -n=" + tmpl["fluxns"], assert: assertGoldenTemplateFile(tc.goldenFile, tmpl), } cmd.runTestCmd(t) }) } } ================================================ FILE: cmd/flux/uninstall.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/uninstall" ) var uninstallCmd = &cobra.Command{ Use: "uninstall", Args: cobra.NoArgs, Short: "Uninstall Flux and its custom resource definitions", Long: `The uninstall command removes the Flux components and the toolkit.fluxcd.io resources from the cluster.`, Example: ` # Uninstall Flux components, its custom resources and namespace flux uninstall --namespace=flux-system # Uninstall Flux but keep the namespace flux uninstall --namespace=infra --keep-namespace=true`, RunE: uninstallCmdRun, } type uninstallFlags struct { keepNamespace bool dryRun bool silent bool } var uninstallArgs uninstallFlags func init() { uninstallCmd.Flags().BoolVar(&uninstallArgs.keepNamespace, "keep-namespace", false, "skip namespace deletion") uninstallCmd.Flags().BoolVar(&uninstallArgs.dryRun, "dry-run", false, "only print the objects that would be deleted") uninstallCmd.Flags().BoolVarP(&uninstallArgs.silent, "silent", "s", false, "delete components without asking for confirmation") rootCmd.AddCommand(uninstallCmd) } func uninstallCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if !uninstallArgs.dryRun && !uninstallArgs.silent { info, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { if !errors.IsNotFound(err) { return fmt.Errorf("cluster info unavailable: %w", err) } } promptLabel := "Are you sure you want to delete Flux and its custom resource definitions" if !installManagedByFlux(info.managedBy) { promptLabel = fmt.Sprintf("Flux is managed by %s! Are you sure you want to delete Flux and its CRDs using Flux CLI", info.managedBy) } prompt := promptui.Prompt{ Label: promptLabel, IsConfirm: true, } if _, err := prompt.Run(); err != nil { return fmt.Errorf("aborting") } } logger.Actionf("deleting components in %s namespace", *kubeconfigArgs.Namespace) uninstall.Components(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) logger.Actionf("deleting toolkit.fluxcd.io finalizers in all namespaces") uninstall.Finalizers(ctx, logger, kubeClient, uninstallArgs.dryRun) logger.Actionf("deleting toolkit.fluxcd.io custom resource definitions") uninstall.CustomResourceDefinitions(ctx, logger, kubeClient, uninstallArgs.dryRun) if !uninstallArgs.keepNamespace { uninstall.Namespace(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) } logger.Successf("uninstall finished") return nil } ================================================ FILE: cmd/flux/version.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "encoding/json" "fmt" "strings" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml/goyaml.v2" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print the client and server-side components version information.", Long: `Print the client and server-side components version information for the current context.`, Example: `# Print client and server-side version flux version # Print only client version flux version --client # Print information in json format flux version -o json `, RunE: versionCmdRun, } type versionFlags struct { client bool output string } var versionArgs versionFlags type versionInfo struct { Flux string `yaml:"flux"` Distribution string `yaml:"distribution,omitempty"` Controller map[string]string `yaml:"controller,inline"` } func init() { versionCmd.Flags().BoolVar(&versionArgs.client, "client", false, "print only client version") versionCmd.Flags().StringVarP(&versionArgs.output, "output", "o", "yaml", "the format in which the information should be printed. can be 'json' or 'yaml'") rootCmd.AddCommand(versionCmd) } func versionCmdRun(cmd *cobra.Command, args []string) error { if versionArgs.output != "yaml" && versionArgs.output != "json" { return fmt.Errorf("--output must be json or yaml, not %s", versionArgs.output) } ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() // versionInfo struct and goyaml is used because we care about the order. // Without this `distribution` is printed before `flux` when the struct is marshalled. info := &versionInfo{ Controller: map[string]string{}, } info.Flux = rootArgs.defaults.Version if !versionArgs.client { kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } clusterInfo, err := getFluxClusterInfo(ctx, kubeClient) // ignoring not found errors because it means that the GitRepository CRD isn't installed but a user might // have other controllers(e.g notification-controller), and we want to still return information for them. if err != nil && !errors.IsNotFound(err) { return err } if clusterInfo.distribution() != "" { info.Distribution = clusterInfo.distribution() } selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} var list v1.DeploymentList if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err != nil { return err } if len(list.Items) == 0 { return fmt.Errorf("no deployments found in %s namespace", *kubeconfigArgs.Namespace) } for _, d := range list.Items { for _, c := range d.Spec.Template.Spec.Containers { name, tag, err := splitImageStr(c.Image) if err != nil { return err } info.Controller[name] = tag } } } var marshalled []byte var err error if versionArgs.output == "json" { marshalled, err = info.toJSON() marshalled = append(marshalled, "\n"...) } else { marshalled, err = yaml.Marshal(&info) } if err != nil { return err } rootCmd.Print(string(marshalled)) return nil } func (info versionInfo) toJSON() ([]byte, error) { mapInfo := map[string]string{ "flux": info.Flux, } if info.Distribution != "" { mapInfo["distribution"] = info.Distribution } for k, v := range info.Controller { mapInfo[k] = v } return json.MarshalIndent(&mapInfo, "", " ") } func splitImageStr(image string) (string, string, error) { ref, err := name.ParseReference(image) if err != nil { return "", "", fmt.Errorf("parsing image '%s' failed: %w", image, err) } reg := ref.Context().RegistryStr() repo := strings.TrimPrefix(image, reg) parts := strings.Split(repo, ":") if len(parts) < 2 { return "", "", fmt.Errorf("missing image tag in image %s", image) } n, t := parts[0], strings.TrimPrefix(repo, parts[0]+":") nameArr := strings.Split(n, "/") return nameArr[len(nameArr)-1], t, nil } ================================================ FILE: cmd/flux/version_test.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" . "github.com/onsi/gomega" ) func TestSplitImageStr(t *testing.T) { tests := []struct { url string expectedName string expectedTag string }{ { url: "fluxcd/notification-controller:v1.0.0", expectedName: "notification-controller", expectedTag: "v1.0.0", }, { url: "ghcr.io/fluxcd/kustomize-controller:v1.0.0", expectedName: "kustomize-controller", expectedTag: "v1.0.0", }, { url: "reg.internal:8080/fluxcd/source-controller:v1.0.0", expectedName: "source-controller", expectedTag: "v1.0.0", }, { url: "fluxcd/source-controller:v1.0.1@sha256:49921d1c7b100650dd654a32df1f6e626b54dfe9707d7bb7bdf43fb7c81f1baf", expectedName: "source-controller", expectedTag: "v1.0.1@sha256:49921d1c7b100650dd654a32df1f6e626b54dfe9707d7bb7bdf43fb7c81f1baf", }, } for _, tt := range tests { g := NewWithT(t) n, t, err := splitImageStr(tt.url) g.Expect(err).To(Not(HaveOccurred())) g.Expect(n).To(BeEquivalentTo(tt.expectedName)) g.Expect(t).To(BeEquivalentTo(tt.expectedTag)) } } ================================================ FILE: cmd/flux/version_utils.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) func getVersion(input string) (string, error) { if input == "" { return rootArgs.defaults.Version, nil } if input != install.MakeDefaultOptions().Version && !strings.HasPrefix(input, "v") { return "", fmt.Errorf("targeted version '%s' must be prefixed with 'v'", input) } if isEmbeddedVersion(input) { return input, nil } var err error if input == install.MakeDefaultOptions().Version { input, err = install.GetLatestVersion() if err != nil { return "", err } } else { if ok, err := install.ExistingVersion(input); err != nil || !ok { if err == nil { err = fmt.Errorf("targeted version '%s' does not exist", input) } return "", err } } if !utils.CompatibleVersion(VERSION, input) { return "", fmt.Errorf("targeted version '%s' is not compatible with your current version of flux (%s)", input, VERSION) } return input, nil } func isEmbeddedVersion(input string) bool { return input == rootArgs.defaults.Version } ================================================ FILE: cmd/flux/version_utils_test.go ================================================ //go:build unit // +build unit /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" ) func TestVersion(t *testing.T) { cmd := cmdTestCase{ args: "--version", assert: assertGoldenValue("flux version 0.0.0-dev.0\n"), } cmd.runTestCmd(t) } func TestVersionCmd(t *testing.T) { tests := []struct { args string expected string }{ { args: "version --client", expected: "flux: v0.0.0-dev.0\n", }, { args: "version --client -o json", expected: "{\n \"flux\": \"v0.0.0-dev.0\"\n}\n", }, } for _, tt := range tests { cmd := cmdTestCase{ args: tt.args, assert: assertGoldenValue(tt.expected), } cmd.runTestCmd(t) } } ================================================ FILE: docs/release/README.md ================================================ # Flux Dev Documentation ## Release specifications - [Flux distribution](https://fluxcd.io/flux/releases/) - [Flux APIs and controllers](https://fluxcd.io/flux/releases/controllers/) - [Flux shared packages](https://fluxcd.io/flux/releases/packages/) - [Flux release procedures](https://fluxcd.io/flux/releases/procedure/) - [Flux release notes template](release-notes-template.md) ================================================ FILE: docs/release/release-notes-template.md ================================================ # Flux release note template This is a template for release notes. It is intended to be used as a starting point for writing release notes for a new release. It should be copied to a temporary file, and then edited to reflect the changes in the release. Once the release notes are complete, you can tag the release and push it to GitHub. After the release is tagged, the CI will build the release artifacts and upload them to the GitHub release page. The release notes can then be copied from the temporary file to the GitHub release page. The release notes should be formatted using [Markdown](https://guides.github.com/features/mastering-markdown/), and not make use of line breaks unless they function as paragraph breaks. For examples of release notes, including language and formatting of the release highlights, see the [Flux release notes](https://github.com/fluxcd/flux2/releases). ## GitHub release template The following template can be used for the GitHub release page: ```markdown ## Highlights ℹ️ Please follow the [Upgrade Procedure for Flux v2.7+](https://github.com/fluxcd/flux2/discussions/5572) for a smooth upgrade from Flux v2.6 to the latest version. ### Fixes and improvements ## New documentation ## Components changelog - -controller [v](https://github.com/fluxcd/-controller/blob//CHANGELOG.md) ## CLI changelog ``` Typically, you want to link the [Flux upgrade](https://fluxcd.io/flux/installation/upgrade/) guide to refer users for up to date information on upgrade options. In some scenarios, you may want to include specific information about API changes and/or upgrade procedures. Consult [the formatting of `v2.0.0-rc.1`](https://github.com/fluxcd/flux2/releases/tag/v2.0.0-rc.1) for such an example. ## Slack message template The following template can be used for the Slack release message: ```markdown :sparkles: *We are pleased to announce the release of Flux [](https://github.com/fluxcd/flux2/releases/tag//)!* :hammer_and_pick: *Fixes and improvements* :books: Documentation :heart: Big thanks to all the Flux contributors that helped us with this release! ``` For more concrete examples, see the pinned messages in the [Flux Slack channel](https://cloud-native.slack.com/archives/CLAJ40HV3). ================================================ FILE: go.mod ================================================ module github.com/fluxcd/flux2/v2 go 1.26.0 // Fix CVE-2022-28948. replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/ProtonMail/go-crypto v1.3.0 github.com/cyphar/filepath-securejoin v0.6.1 github.com/distribution/distribution/v3 v3.0.0 github.com/fluxcd/cli-utils v0.37.2-flux.1 github.com/fluxcd/go-git-providers v0.26.0 github.com/fluxcd/helm-controller/api v1.5.3 github.com/fluxcd/image-automation-controller/api v1.1.1 github.com/fluxcd/image-reflector-controller/api v1.1.1 github.com/fluxcd/kustomize-controller/api v1.8.2 github.com/fluxcd/notification-controller/api v1.8.2 github.com/fluxcd/pkg/apis/event v0.25.0 github.com/fluxcd/pkg/apis/meta v1.26.0 github.com/fluxcd/pkg/auth v0.40.0 github.com/fluxcd/pkg/chartutil v1.23.0 github.com/fluxcd/pkg/envsubst v1.5.0 github.com/fluxcd/pkg/git v0.46.0 github.com/fluxcd/pkg/kustomize v1.28.0 github.com/fluxcd/pkg/oci v0.63.0 github.com/fluxcd/pkg/runtime v0.103.0 github.com/fluxcd/pkg/sourceignore v0.17.0 github.com/fluxcd/pkg/ssa v0.70.0 github.com/fluxcd/pkg/ssh v0.24.0 github.com/fluxcd/pkg/tar v0.17.0 github.com/fluxcd/pkg/version v0.14.0 github.com/fluxcd/source-controller/api v1.8.1 github.com/fluxcd/source-watcher/api/v2 v2.1.1 github.com/go-git/go-git/v5 v5.16.5 github.com/go-logr/logr v1.4.3 github.com/gonvenience/bunt v1.4.2 github.com/gonvenience/ytbx v1.4.7 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.7 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/homeport/dyff v1.10.2 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-shellwords v1.0.12 github.com/notaryproject/notation-go v1.3.2 github.com/olekukonko/tablewriter v0.0.5 github.com/onsi/gomega v1.39.1 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/spf13/cobra v1.10.2 github.com/theckman/yacspin v0.13.12 golang.org/x/crypto v0.48.0 golang.org/x/term v0.40.0 golang.org/x/text v0.34.0 k8s.io/api v0.35.2 k8s.io/apiextensions-apiserver v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/cli-runtime v0.35.2 k8s.io/client-go v0.35.2 k8s.io/kubectl v0.35.2 sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/kustomize/api v0.21.1 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) require ( cloud.google.com/go/auth v0.18.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect code.gitea.io/sdk/gitea v0.23.2 // indirect dario.cat/mergo v1.0.1 // indirect github.com/42wim/httpsig v1.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9 // indirect github.com/aws/aws-sdk-go-v2/service/eks v1.77.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.2.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect github.com/fluxcd/pkg/cache v0.13.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-ldap/ldap/v3 v3.4.10 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/gonvenience/idem v0.0.2 // indirect github.com/gonvenience/neat v1.3.16 // indirect github.com/gonvenience/term v1.0.4 // indirect github.com/gonvenience/text v1.0.9 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-github/v82 v82.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/notaryproject/notation-core-go v1.3.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xlab/treeprint v1.2.0 // indirect gitlab.com/gitlab-org/api/client-go v1.29.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect go.opentelemetry.io/otel/log v0.16.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/api v0.261.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v4 v4.1.3 // indirect k8s.io/component-base v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k= github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 h1:figxyQZXzZQIcP3njhC68bYUiTw45J8/SsHaLW8Ax0M= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0/go.mod h1:TmlMW4W5OvXOmOyKNnor8nlMMiO1ctIyzmHme/VHsrA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 h1:B7f9R99lCF83XlolTg6d6Lvghyto+/VU83ZrneAVfK8= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1/go.mod h1:cpYRXx5BkmS3mwWRKPbWSPKmyAUNL7aLWAPiiinwk/U= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9 h1:WxoqdNfGWj668u/NX7qBMPevmJu14LYNMMTRZthoclc= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9/go.mod h1:4oMS/bVKMnYIIBgkcHPoru4DVeMGutHv03FZUTjvsvI= github.com/aws/aws-sdk-go-v2/service/eks v1.77.0 h1:Z5mTpmbJKU7jEM7xoXI5tO4Nm0JUZSgVSFkpYuu6Ic0= github.com/aws/aws-sdk-go-v2/service/eks v1.77.0/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/elazarl/goproxy v1.8.0 h1:dt561rX7UAYMeFRLtzFx6uQGl2TpL1dr6uCG23nFQSY= github.com/elazarl/goproxy v1.8.0/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0= github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c= github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= github.com/fluxcd/go-git-providers v0.26.0 h1:0DUsXc1nS9Fe4n8tXSEUCGemWzHShd66gmotayDPekw= github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo= github.com/fluxcd/helm-controller/api v1.5.3 h1:ruLzuyTHjjE9A5B/U+Id2q7yHXXqSFTswdZ14xCS5So= github.com/fluxcd/helm-controller/api v1.5.3/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI= github.com/fluxcd/image-automation-controller/api v1.1.1 h1:uiu7kjdVoW8/461HOemX6I7RcPornEzQliWgTg6LnWI= github.com/fluxcd/image-automation-controller/api v1.1.1/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660= github.com/fluxcd/image-reflector-controller/api v1.1.1 h1:4Bj1abzVnjj8+b/293kNeFMRJc+y2wO8Z12ReZ/gA0w= github.com/fluxcd/image-reflector-controller/api v1.1.1/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0= github.com/fluxcd/kustomize-controller/api v1.8.2 h1:LcFUjJccwNrhCo7pQBBneLAlHfZZcb58bWB2LnyFwag= github.com/fluxcd/kustomize-controller/api v1.8.2/go.mod h1:c/mUPIffDDLg1EicXCJtX4N/rc+z5Zh0e/CXjhd7Dyc= github.com/fluxcd/notification-controller/api v1.8.2 h1:TDrXohUC5Gh3BF+v2ux9/zEG1Ax8u49WDW+3Y6GiIEc= github.com/fluxcd/notification-controller/api v1.8.2/go.mod h1:ozgJGQPy0dG5eOsLZlwAr6n0q/y6+TWd1fGOtavlXJA= github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA= github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4= github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE= github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y= github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE= github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= github.com/fluxcd/pkg/auth v0.40.0 h1:p6Kw6KH+z8oRqngKhmTt8ILKD/rC+8tP87a//kLZhi8= github.com/fluxcd/pkg/auth v0.40.0/go.mod h1:Oq/hIEKUMTbL2bv5blf+EhC/jXXJLsOjIMtJj/AtG3Y= github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk= github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs= github.com/fluxcd/pkg/chartutil v1.23.0 h1:ohstQEVnrBIbN85FGu83hnmAohLl0PdOoPlsM6+cjyI= github.com/fluxcd/pkg/chartutil v1.23.0/go.mod h1:kFhmD6DwBgRsvC1ilINsomargMi2WbqvSndWQLikkLc= github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY= github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes= github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw= github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI= github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8= github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= github.com/fluxcd/pkg/kustomize v1.28.0 h1:0RuFVczJRabbt8frHZ/ql8aqte6BOOKk274O09l6/hE= github.com/fluxcd/pkg/kustomize v1.28.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= github.com/fluxcd/pkg/oci v0.63.0 h1:ZPKTT2C+gWYjhP63xC76iTPdYE9w3ABcsDq77uhAgwo= github.com/fluxcd/pkg/oci v0.63.0/go.mod h1:qMPz4njvm6hJzdyGSb8ydSqrapXxTQwJonxHIsdeXSQ= github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw= github.com/fluxcd/pkg/sourceignore v0.17.0 h1:Z72nruRMhC15zIEpWoDrAcJcJ1El6QDnP/aRDfE4WOA= github.com/fluxcd/pkg/sourceignore v0.17.0/go.mod h1:3e/VmYLId0pI/H5sK7W9Ibif+j0Ahns9RxNjDMtTTfY= github.com/fluxcd/pkg/ssa v0.70.0 h1:IBylYPiTK1IEdCC2DvjKXIhwQcbd5VufXA9WS3zO+tE= github.com/fluxcd/pkg/ssa v0.70.0/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg= github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4= github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg= github.com/fluxcd/pkg/tar v0.17.0 h1:uNxbFXy8ly8C7fJ8D7w3rjTNJFrb4Hp1aY/30XkfvxY= github.com/fluxcd/pkg/tar v0.17.0/go.mod h1:b1xyIRYDD0ket4SV5u0UXYv+ZdN/O/HmIO5jZQdHQls= github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw= github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= github.com/fluxcd/source-controller/api v1.8.1 h1:49HiJF5mNEdZTwueQMRahTVts35B+xhN5CsuOAL9gQ0= github.com/fluxcd/source-controller/api v1.8.1/go.mod h1:HgZ6NSH1cyOE2jRoNwln1xEwr9ETvrLeiy1o4O04vQM= github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI= github.com/fluxcd/source-watcher/api/v2 v2.1.1/go.mod h1:6M1BzBGQRoIuSenSQlfJHwMVVobFPiNPxXqfN0IILc4= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gonvenience/bunt v1.4.2 h1:nTgkFZsw38SIJKABhLj8aXj2rqion9Zo1so/EBkbFBY= github.com/gonvenience/bunt v1.4.2/go.mod h1:WjyEO2rSYR+OLZg67Ucl+gjdXPs8GpFl63SCA02XDyI= github.com/gonvenience/idem v0.0.2 h1:jWHknjPfSbiWgYKre9wB2FhMgVLd1RWXCXzVq+7VIWg= github.com/gonvenience/idem v0.0.2/go.mod h1:0Xv1MpnNL40+dsyOxaJFa7L8ekeTRr63WaWXpiWLFFM= github.com/gonvenience/neat v1.3.16 h1:Vb0iCkSHGWaA+ry69RY3HpQ6Ooo6o/g2wjI80db8DjI= github.com/gonvenience/neat v1.3.16/go.mod h1:sLxdQNNluxbpROxTTHs3XBSJX8fwFX5toEULUy74ODA= github.com/gonvenience/term v1.0.4 h1:qkCGfmUtpzs9W4jWgNijaGF6dg3oSIh+kZCzT5cPNZY= github.com/gonvenience/term v1.0.4/go.mod h1:OzNdQC5NVBou9AifaHd1QG6EP8iDdpaT7GFm1bVgslg= github.com/gonvenience/text v1.0.9 h1:U29BxT3NZnNPcfiEnAwt6yHXe38fQs2Q+WTqs1X+atI= github.com/gonvenience/text v1.0.9/go.mod h1:JQF1ifXNRaa66jnPLqoITA+y8WATlG0eJzFC9ElJS3s= github.com/gonvenience/ytbx v1.4.7 h1:3wJ7EOfdv3Lg+h0mzKo7f8d1zMY1EJtVzzYrA3UhjHQ= github.com/gonvenience/ytbx v1.4.7/go.mod h1:ZmAU727eOTYeC4aUJuqyb9vogNAN7NiSKfw6Aoxbqys= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk= github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/homeport/dyff v1.10.2 h1:XyB+D0KVwjbUFTZYIkvPtsImwkfh+ObH2CEdEHTqdr4= github.com/homeport/dyff v1.10.2/go.mod h1:0kIjL/JOGaXigzrLY6kcl5esSStbAa99r6GzEvr7lrs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs= github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64= github.com/notaryproject/notation-go v1.3.2 h1:4223iLXOHhEV7ZPzIUJEwwMkhlgzoYFCsMJvSH1Chb8= github.com/notaryproject/notation-go v1.3.2/go.mod h1:/1kuq5WuLF6Gaer5re0Z6HlkQRlKYO4EbWWT/L7J1Uw= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/gitlab-org/api/client-go v1.29.0 h1:3KnF6vENry/9v9eVrnLi2OfBV0m/WSrwh3RcxgH/hkA= gitlab.com/gitlab-org/api/client-go v1.29.0/go.mod h1:6i3EZtC6gKiTTmDwp+f6r/Yi9OY4AaYubl5B3yXEdHE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.261.0 h1:3DoJ2GGibaCxNi1lhdScNMx9fTW87ujKHDgyHMMYdoA= google.golang.org/api v0.261.0/go.mod h1:nVH0ZK5C4tO0RdsMscleeTLY7I8m/Nt9IXxcXD2tfts= google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= helm.sh/helm/v4 v4.1.3 h1:Abfmb+oJUtxoaXDyB2Jhw1zRk3hT6aFfHta+AXb8Lno= helm.sh/helm/v4 v4.1.3/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0= k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E= k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc= k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kubectl v0.35.2 h1:aSmqhSOfsoG9NR5oR8OD5eMKpLN9x8oncxfqLHbJJII= k8s.io/kubectl v0.35.2/go.mod h1:+OJC779UsDJGxNPbHxCwvb4e4w9Eh62v/DNYU2TlsyM= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: install/README.md ================================================ # flux CLI Installation Binaries for macOS and Linux AMD64 are available for download on the [release page](https://github.com/fluxcd/flux2/releases). To install the latest release run: ```bash curl -s https://raw.githubusercontent.com/fluxcd/flux2/main/install/flux.sh | sudo bash ``` **Note**: You may want to export the `GITHUB_TOKEN` environment variable using a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) to avoid GitHub API rate-limiting errors if executing the install script repeatedly during a short time frame. The install script does the following: * attempts to detect your OS * downloads and unpacks the release tar file in a temporary directory * copies the flux binary to `/usr/local/bin` * removes the temporary directory If you want to use flux as a kubectl plugin, rename the binary to `kubectl-flux`: ```sh mv /usr/local/bin/flux /usr/local/bin/kubectl-flux ``` ## Build from source Clone the repository: ```bash git clone https://github.com/fluxcd/flux2 cd flux2 ``` Build the `flux` binary (requires go >= 1.15): ```bash make build ``` Run the binary: ```bash ./bin/flux -h ``` ================================================ FILE: install/flux.sh ================================================ #!/usr/bin/env bash set -e DEFAULT_BIN_DIR="/usr/local/bin" BIN_DIR=${1:-"${DEFAULT_BIN_DIR}"} GITHUB_REPO="fluxcd/flux2" # Helper functions for logs info() { echo '[INFO] ' "$@" } warn() { echo '[WARN] ' "$@" >&2 } fatal() { echo '[ERROR] ' "$@" >&2 exit 1 } # Set os, fatal if operating system not supported setup_verify_os() { if [[ -z "${OS}" ]]; then OS=$(uname) fi case ${OS} in Darwin) OS=darwin ;; Linux) OS=linux ;; *) fatal "Unsupported operating system ${OS}" esac } # Set arch, fatal if architecture not supported setup_verify_arch() { if [[ -z "${ARCH}" ]]; then ARCH=$(uname -m) fi case ${ARCH} in arm|armv6l|armv7l) ARCH=arm ;; arm64|aarch64|armv8l) ARCH=arm64 ;; amd64) ARCH=amd64 ;; x86_64) ARCH=amd64 ;; *) fatal "Unsupported architecture ${ARCH}" esac } # Verify existence of downloader executable verify_downloader() { # Return failure if it doesn't exist or is no executable [[ -x "$(which "$1")" ]] || return 1 # Set verified executable as our downloader program and return success DOWNLOADER=$1 return 0 } # Create tempory directory and cleanup when done setup_tmp() { TMP_DIR=$(mktemp -d -t flux-install.XXXXXXXXXX) TMP_METADATA="${TMP_DIR}/flux.json" TMP_HASH="${TMP_DIR}/flux.hash" TMP_BIN="${TMP_DIR}/flux.tar.gz" cleanup() { local code=$? set +e trap - EXIT rm -rf "${TMP_DIR}" exit ${code} } trap cleanup INT EXIT } # Find version from Github metadata get_release_version() { if [[ -n "${FLUX_VERSION}" ]]; then SUFFIX_URL="tags/v${FLUX_VERSION}" else SUFFIX_URL="latest" fi METADATA_URL="https://api.github.com/repos/${GITHUB_REPO}/releases/${SUFFIX_URL}" info "Downloading metadata ${METADATA_URL}" download "${TMP_METADATA}" "${METADATA_URL}" VERSION_FLUX=$(grep '"tag_name":' "${TMP_METADATA}" | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) if [[ -n "${VERSION_FLUX}" ]]; then info "Using ${VERSION_FLUX} as release" else fatal "Unable to determine release version" fi } # Download from file from URL download() { [[ $# -eq 2 ]] || fatal 'download needs exactly 2 arguments' case $DOWNLOADER in curl) curl -u user:$GITHUB_TOKEN -o "$1" -sfL "$2" ;; wget) wget --auth-no-challenge --user=user --password=$GITHUB_TOKEN -qO "$1" "$2" ;; *) fatal "Incorrect executable '${DOWNLOADER}'" ;; esac # Abort if download command failed [[ $? -eq 0 ]] || fatal 'Download failed' } # Version comparison # Returns 0 on '=', 1 on '>', and 2 on '<'. # Ref: https://stackoverflow.com/a/4025065 vercomp () { if [[ $1 == $2 ]] then return 0 fi local IFS=. local i ver1=($1) ver2=($2) # fill empty fields in ver1 with zeros for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) do ver1[i]=0 done for ((i=0; i<${#ver1[@]}; i++)) do if [[ -z ${ver2[i]} ]] then # fill empty fields in ver2 with zeros ver2[i]=0 fi if ((10#${ver1[i]} > 10#${ver2[i]})) then return 1 fi if ((10#${ver1[i]} < 10#${ver2[i]})) then return 2 fi done return 0 } # Download hash from Github URL download_hash() { HASH_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION_FLUX}/flux_${VERSION_FLUX}_checksums.txt" # NB: support the checksum filename format prior to v0.6.0 set +e vercomp ${VERSION_FLUX} 0.6.0 if [[ $? -eq 2 ]]; then HASH_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION_FLUX}/flux2_${VERSION_FLUX}_checksums.txt" fi set -e info "Downloading hash ${HASH_URL}" download "${TMP_HASH}" "${HASH_URL}" HASH_EXPECTED=$(grep " flux_${VERSION_FLUX}_${OS}_${ARCH}.tar.gz$" "${TMP_HASH}") HASH_EXPECTED=${HASH_EXPECTED%%[[:blank:]]*} } # Download binary from Github URL download_binary() { BIN_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION_FLUX}/flux_${VERSION_FLUX}_${OS}_${ARCH}.tar.gz" info "Downloading binary ${BIN_URL}" download "${TMP_BIN}" "${BIN_URL}" } compute_sha256sum() { cmd=$(which sha256sum shasum | head -n 1) case $(basename "$cmd") in sha256sum) sha256sum "$1" | cut -f 1 -d ' ' ;; shasum) shasum -a 256 "$1" | cut -f 1 -d ' ' ;; *) fatal "Can not find sha256sum or shasum to compute checksum" ;; esac } # Verify downloaded binary hash verify_binary() { info "Verifying binary download" HASH_BIN=$(compute_sha256sum "${TMP_BIN}") HASH_BIN=${HASH_BIN%%[[:blank:]]*} if [[ "${HASH_EXPECTED}" != "${HASH_BIN}" ]]; then fatal "Download sha256 does not match ${HASH_EXPECTED}, got ${HASH_BIN}" fi } # Setup permissions and move binary setup_binary() { chmod 755 "${TMP_BIN}" info "Installing flux to ${BIN_DIR}/flux" tar -xzof "${TMP_BIN}" -C "${TMP_DIR}" local CMD_MOVE="mv -f \"${TMP_DIR}/flux\" \"${BIN_DIR}\"" if [[ -w "${BIN_DIR}" ]]; then eval "${CMD_MOVE}" else eval "sudo ${CMD_MOVE}" fi } # Run the install process { setup_verify_os setup_verify_arch verify_downloader curl || verify_downloader wget || fatal 'Can not find curl or wget for downloading files' setup_tmp get_release_version download_hash download_binary verify_binary setup_binary } ================================================ FILE: internal/build/build.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package build import ( "bytes" "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "strings" "sync" "time" "github.com/theckman/yacspin" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kyaml/yaml" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/kustomize" runclient "github.com/fluxcd/pkg/runtime/client" ssautil "github.com/fluxcd/pkg/ssa/utils" "sigs.k8s.io/kustomize/kyaml/filesys" "github.com/fluxcd/flux2/v2/internal/utils" ) const ( controllerName = "kustomize-controller" controllerGroup = "kustomize.toolkit.fluxcd.io" mask = "**SOPS**" dockercfgSecretType = "kubernetes.io/dockerconfigjson" typeField = "type" dataField = "data" stringDataField = "stringData" spinnerDryRunMessage = "running dry-run" ) var defaultTimeout = 80 * time.Second // Builder builds yaml manifests // It retrieves the kustomization object from the k8s cluster // and overlays the manifests with the resources specified in the resourcesPath type Builder struct { client client.WithWatch restMapper meta.RESTMapper name string namespace string resourcesPath string kustomizationFile string ignore []string // mu is used to synchronize access to the kustomization file mu sync.Mutex action kustomize.Action kustomization *kustomizev1.Kustomization timeout time.Duration spinner *yacspin.Spinner dryRun bool strictSubst bool recursive bool localSources map[string]string // diff needs to handle kustomizations one by one singleKustomization bool } // BuilderOptionFunc is a function that configures a Builder type BuilderOptionFunc func(b *Builder) error // WithKustomizationFile sets the kustomization file func WithKustomizationFile(file string) BuilderOptionFunc { return func(b *Builder) error { b.kustomizationFile = file return nil } } // WithTimeout sets the timeout for the builder func WithTimeout(timeout time.Duration) BuilderOptionFunc { return func(b *Builder) error { b.timeout = timeout return nil } } func WithProgressBar() BuilderOptionFunc { return func(b *Builder) error { // Add a spinner cfg := yacspin.Config{ Frequency: 100 * time.Millisecond, CharSet: yacspin.CharSets[59], Suffix: "Kustomization diffing...", SuffixAutoColon: true, Message: spinnerDryRunMessage, StopCharacter: "✓", StopColors: []string{"fgGreen"}, } spinner, err := yacspin.New(cfg) if err != nil { return fmt.Errorf("failed to create spinner: %w", err) } b.spinner = spinner return nil } } // WithClientConfig sets the client configuration func WithClientConfig(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Options) BuilderOptionFunc { return func(b *Builder) error { kubeClient, err := utils.KubeClient(rcg, clientOpts) if err != nil { return err } restMapper, err := rcg.ToRESTMapper() if err != nil { return err } b.client = kubeClient b.restMapper = restMapper b.namespace = *rcg.Namespace return nil } } // WithNamespace sets the namespace func WithNamespace(namespace string) BuilderOptionFunc { return func(b *Builder) error { b.namespace = namespace return nil } } // WithDryRun sets the dry-run flag func WithDryRun(dryRun bool) BuilderOptionFunc { return func(b *Builder) error { b.dryRun = dryRun return nil } } // WithStrictSubstitute sets the strict substitute flag func WithStrictSubstitute(strictSubstitute bool) BuilderOptionFunc { return func(b *Builder) error { b.strictSubst = strictSubstitute return nil } } // WithIgnore sets ignore field func WithIgnore(ignore []string) BuilderOptionFunc { return func(b *Builder) error { b.ignore = ignore return nil } } // WithRecursive sets the recursive field func WithRecursive(recursive bool) BuilderOptionFunc { return func(b *Builder) error { b.recursive = recursive return nil } } // WithLocalSources sets the local sources field func WithLocalSources(localSources map[string]string) BuilderOptionFunc { return func(b *Builder) error { b.localSources = localSources return nil } } // WithSingleKustomization sets the single kustomization field to true func WithSingleKustomization() BuilderOptionFunc { return func(b *Builder) error { b.singleKustomization = true return nil } } // withClientConfigFrom copies client and restMapper fields func withClientConfigFrom(in *Builder) BuilderOptionFunc { return func(b *Builder) error { b.client = in.client b.restMapper = in.restMapper return nil } } // withClientConfigFrom copies spinner field func withSpinnerFrom(in *Builder) BuilderOptionFunc { return func(b *Builder) error { b.spinner = in.spinner return nil } } // withKustomization sets the kustomization field func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc { return func(b *Builder) error { b.kustomization = k return nil } } // NewBuilder returns a new Builder // It takes a kustomization name and a path to the resources // It also takes a list of BuilderOptionFunc to configure the builder // One of the options is WithClientConfig, that must be provided for the builder to work // with the k8s cluster // One other option is WithKustomizationFile, that must be provided for the builder to work // with a local kustomization file. If the kustomization file is not provided, the builder // will try to retrieve the kustomization object from the k8s cluster. // WithDryRun sets the dry-run flag, and needs to be provided if the builder is used for // a dry-run. This flag works in conjunction with WithKustomizationFile, because the // kustomization object is not retrieved from the k8s cluster when the dry-run flag is set. func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { b := &Builder{ name: name, resourcesPath: resources, } for _, opt := range opts { if err := opt(b); err != nil { return nil, err } } if b.timeout == 0 { b.timeout = defaultTimeout } if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil { return nil, fmt.Errorf("kustomization file is required for dry-run") } if !b.dryRun && b.client == nil { return nil, fmt.Errorf("client is required for live run") } return b, nil } func (b *Builder) resolveKustomization(liveKus *kustomizev1.Kustomization) (k *kustomizev1.Kustomization, err error) { // local kustomization file takes precedence over live kustomization if b.kustomizationFile != "" { k, err = b.unMarshallKustomization() if err != nil { return } if !b.dryRun && liveKus != nil && liveKus.Status.Inventory != nil { // merge the live kustomization status with the local kustomization in order to get the // live resources status k.Status = *liveKus.Status.DeepCopy() } } else { k = liveKus } return } func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) { liveKus := &kustomizev1.Kustomization{} namespacedName := types.NamespacedName{ Namespace: b.namespace, Name: b.name, } err := b.client.Get(ctx, namespacedName, liveKus) if err != nil { return nil, err } return liveKus, nil } // Build builds the yaml manifests from the kustomization object // and overlays the manifests with the resources specified in the resourcesPath // It expects a kustomization.yaml file in the resourcesPath, and it will // generate a kustomization.yaml file if it doesn't exist func (b *Builder) Build() ([]*unstructured.Unstructured, error) { m, err := b.build() if err != nil { return nil, err } resources, err := m.AsYaml() if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) } objects, err := ssautil.ReadObjects(bytes.NewReader(resources)) if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) } if m := b.kustomization.Spec.CommonMetadata; m != nil { ssautil.SetCommonMetadata(objects, m.Labels, m.Annotations) } if b.recursive && !b.singleKustomization { var objectsToAdd []*unstructured.Unstructured for _, obj := range objects { if isKustomization(obj) { k, err := toKustomization(obj) if err != nil { return nil, err } if !kustomizationsEqual(k, b.kustomization) { subObjects, err := b.kustomizationBuild(k) if err != nil { return nil, err } objectsToAdd = append(objectsToAdd, subObjects...) } } } objects = append(objects, objectsToAdd...) } return objects, nil } func (b *Builder) build() (m resmap.ResMap, err error) { ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() // Get the kustomization object liveKus := &kustomizev1.Kustomization{} if b.dryRun { liveKus = b.kustomization } else { liveKus, err = b.getKustomization(ctx) if err != nil { if !apierrors.IsNotFound(err) || b.kustomization == nil { return nil, fmt.Errorf("failed to get kustomization object: %w", err) } // use provided Kustomization liveKus = b.kustomization } } k, err := b.resolveKustomization(liveKus) if err != nil { err = fmt.Errorf("failed to get kustomization object: %w", err) return } // store the kustomization object b.kustomization = k // generate kustomization.yaml if needed action, er := b.generate(*k, b.resourcesPath) if er != nil { errf := kustomize.CleanDirectory(b.resourcesPath, action) err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) return } b.action = action defer func() { errf := b.Cancel() if err == nil { err = errf } }() // build the kustomization m, err = b.do(ctx, *k, b.resourcesPath) if err != nil { return } for _, res := range m.Resources() { // set owner labels err = b.setOwnerLabels(res) if err != nil { return } // make sure secrets are masked err = maskSopsData(res) if err != nil { return } } return } func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructured.Unstructured, error) { resourcesPath, err := b.kustomizationPath(k) if err != nil { return nil, err } subBuilder, err := NewBuilder(k.Name, resourcesPath, // use same client withClientConfigFrom(b), // kustomization will be used if there is no live kustomization withKustomization(k), WithTimeout(b.timeout), WithNamespace(k.Namespace), WithIgnore(b.ignore), WithStrictSubstitute(b.strictSubst), WithRecursive(b.recursive), WithLocalSources(b.localSources), WithDryRun(b.dryRun), ) if err != nil { return nil, err } return subBuilder.Build() } func (b *Builder) kustomizationPath(k *kustomizev1.Kustomization) (string, error) { sourceRef := k.Spec.SourceRef.DeepCopy() if sourceRef.Namespace == "" { sourceRef.Namespace = k.Namespace } sourceKey := sourceRef.String() localPath, ok := b.localSources[sourceKey] if !ok { return "", fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, k.Name) } return filepath.Join(localPath, k.Spec.Path), nil } func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) { data, err := os.ReadFile(b.kustomizationFile) if err != nil { return nil, fmt.Errorf("failed to read kustomization file %s: %w", b.kustomizationFile, err) } k := &kustomizev1.Kustomization{} decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data)) // check for kustomization in yaml with the same name and namespace for { // ensure the target struct is emptied before decoding k = &kustomizev1.Kustomization{} err = decoder.Decode(k) if err != nil { if err == io.EOF { return nil, fmt.Errorf("failed find kustomization with name '%s' and namespace '%s' in file '%s'", b.name, b.namespace, b.kustomizationFile) } else { return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err) } } if strings.HasPrefix(k.APIVersion, kustomizev1.GroupVersion.Group+"/") && k.Kind == kustomizev1.KustomizationKind && k.Name == b.name && (k.Namespace == b.namespace || k.Namespace == "") { break } } return k, nil } func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil { return "", err } // a scanner will be used down the line to parse the list // so we have to make sure to include newlines ignoreList := strings.Join(b.ignore, "\n") gen := kustomize.NewGeneratorWithIgnore("", ignoreList, unstructured.Unstructured{Object: data}) // acquire the lock b.mu.Lock() defer b.mu.Unlock() return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) } func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { fs := filesys.MakeFsOnDisk() // acquire the lock b.mu.Lock() defer b.mu.Unlock() m, err := kustomize.Build(fs, dirPath) if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) } if kustomization.Spec.PostBuild == nil { return m, nil } data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil { return nil, err } for _, res := range m.Resources() { // run variable substitutions outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res, kustomize.SubstituteWithDryRun(b.dryRun), kustomize.SubstituteWithStrict(b.strictSubst), ) if err != nil { return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) } if outRes != nil { _, err = m.Replace(res) if err != nil { return nil, err } } } return m, nil } func isKustomization(object *unstructured.Unstructured) bool { return strings.HasPrefix(object.GetAPIVersion(), kustomizev1.GroupVersion.Group+"/") && object.GetKind() == kustomizev1.KustomizationKind } func toKustomization(object *unstructured.Unstructured) (*kustomizev1.Kustomization, error) { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object) if err != nil { return nil, fmt.Errorf("failed to convert to unstructured: %w", err) } k := &kustomizev1.Kustomization{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj, k) if err != nil { return nil, fmt.Errorf("failed to convert to kustomization: %w", err) } return k, nil } func kustomizationsEqual(k1 *kustomizev1.Kustomization, k2 *kustomizev1.Kustomization) bool { return k1.Name == k2.Name && k1.Namespace == k2.Namespace } func (b *Builder) setOwnerLabels(res *resource.Resource) error { labels := res.GetLabels() labels[controllerGroup+"/name"] = b.kustomization.GetName() labels[controllerGroup+"/namespace"] = b.kustomization.GetNamespace() err := res.SetLabels(labels) if err != nil { return err } return nil } 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() 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 the intended type is Opaque, then it can be omitted from the manifest, since it's the default // Ref: https://kubernetes.io/docs/concepts/configuration/secret/#opaque-secrets if errors.As(err, &yaml.NoFieldError{}) { secretType = "Opaque" } else if err != nil { return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) } 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, true) if err != nil { return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) } err = maskDockerconfigjsonSopsData(stringDataMap, false) 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] = mask } } } 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, encode bool) error { sopsMess := struct { Mask string `json:"mask"` }{ Mask: mask, } maskJson, err := json.Marshal(sopsMess) if err != nil { return err } if encode { for k := range dataMap { dataMap[k] = base64.StdEncoding.EncodeToString(maskJson) } return nil } for k := range dataMap { dataMap[k] = string(maskJson) } return nil } func maskBase64EncryptedSopsData(dataMap map[string]string, mask string) error { for k, v := range dataMap { data, err := base64.StdEncoding.DecodeString(v) if corruptErr := base64.CorruptInputError(0); errors.As(err, &corruptErr) { return corruptErr } 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 } // Cancel cancels the build // It restores a clean repository func (b *Builder) Cancel() error { // acquire the lock b.mu.Lock() defer b.mu.Unlock() err := kustomize.CleanDirectory(b.resourcesPath, b.action) if err != nil { return err } return nil } func (b *Builder) StartSpinner() error { if b.spinner == nil { return nil } err := b.spinner.Start() if err != nil { return fmt.Errorf("failed to start spinner: %w", err) } return nil } func (b *Builder) StopSpinner() error { if b.spinner == nil { return nil } status := b.spinner.Status() if status == yacspin.SpinnerRunning || status == yacspin.SpinnerPaused { err := b.spinner.Stop() if err != nil { return fmt.Errorf("failed to stop spinner: %w", err) } } return nil } ================================================ FILE: internal/build/build_test.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package build import ( "fmt" "strings" "testing" "time" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kyaml/yaml" ) func TestTrimSopsData(t *testing.T) { testCases := []struct { name string yamlStr string expected string }{ { name: "secret with sops token", yamlStr: `apiVersion: v1 kind: Secret metadata: name: my-secret type: Opaque data: token: | ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpvQmU1UGxQbWZRQ1VVYzRzcUtJbW p3PT0saXY6TUxMRVcxNVFDOWtSZFZWYWdKbnpMQ1NrMHhaR1dJcEFlVGZIenl4VDEwZz0s dGFnOkszR2tCQ0dTK3V0NFRwazZuZGIwQ0E9PSx0eXBlOnN0cl0iLAoJInNvcHMiOiB7Cg kJImttcyI6IG51bGwsCgkJImdjcF9rbXMiOiBudWxsLAoJCSJhenVyZV9rdiI6IG51bGws CgkJImhjX3ZhdWx0IjogbnVsbCwKCQkiYWdlIjogWwoJCQl7CgkJCQkicmVjaXBpZW50Ij ogImFnZTEwbGEyZ2Uwd3R2eDNxcjdkYXRxZjdyczR5bmd4c3pkYWw5MjdmczlydWthbXI4 dTJwc2hzdnR6N2NlIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIE ZJTEUtLS0tLVxuWVdkbExXVnVZM0o1Y0hScGIyNHViM0puTDNZeENpMCtJRmd5TlRVeE9T QTFMMlJwWkhScksxRlNWbVlyZDFWYVxuWTBoeFdGUXpTREJzVDFrM1dqTnRZbVUxUW1saW FESnljWGxOQ25GMVlqZE5PVGhWYlZOdk1HOXJOUzlaVVhad1xuTW5WMGJuUlVNR050ZWpG UGJ6TTRVMlV6V2tzemVWa0tMUzB0SUdKNlVHaHhNVVYzWW1WSlRIbEpTVUpwUlZSWlxuVm pkMFJWUmFkVTh3ZWt4WFRISXJZVXBsWWtOMmFFRUswSS9NQ0V0WFJrK2IvTjJHMUpGM3ZI UVQyNGRTaFdZRFxudytKSVVTQTNhTGYyc3YwenIyTWRVRWRWV0JKb004blQ0RDR4VmJCT1 JEKzY2OVcrOW5EZVN3PT1cbi0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG4i CgkJCX0KCQldLAoJCSJsYXN0bW9kaWZpZWQiOiAiMjAyMS0xMS0yNlQxNjozNDo1MVoiLA oJCSJtYWMiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpDT0d6ZjVZQ0hOTlA2ejRKYUVLcmpO M004ZjUrUTF1S1VLVE1Id2ozODgvSUNtTHlpMnNTclRtajdQUCtYN005alRWd2E4d1ZnWV RwTkxpVkp4K0xjeHF2SVhNMFR5bysvQ3UxenJmYW85OGFpQUNQOCtUU0VEaUZRTnRFdXMy M0grZC9YMWhxTXdSSERJM2tRKzZzY2dFR25xWTU3cjNSRFNBM0U4RWhIcjQ9LGl2Okx4aX RWSVltOHNyWlZxRnVlSmg5bG9DbEE0NFkyWjNYQVZZbXhlc01tT2c9LHRhZzpZOHFGRDhV R2xEZndOU3Y3eGxjbjZBPT0sdHlwZTpzdHJdIiwKCQkicGdwIjogbnVsbCwKCQkidW5lbm NyeXB0ZWRfc3VmZml4IjogIl91bmVuY3J5cHRlZCIsCgkJInZlcnNpb24iOiAiMy43LjEi Cgl9Cn0= `, expected: `apiVersion: v1 data: token: KipTT1BTKio= kind: Secret metadata: name: my-secret type: Opaque `, }, { name: "secret with basic auth", yamlStr: `apiVersion: v1 data: password: cGFzc3dvcmQK username: YWRtaW4K kind: Secret metadata: name: secret-basic-auth type: kubernetes.io/basic-auth `, expected: `apiVersion: v1 data: password: cGFzc3dvcmQK username: YWRtaW4K 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 `, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r, err := yaml.Parse(tc.yamlStr) if err != nil { t.Fatalf("unable to parse yaml: %v", err) } resource := &resource.Resource{RNode: *r} err = maskSopsData(resource) if err != nil { t.Fatalf("unable to trim sops data: %v", err) } sYaml, err := resource.AsYAML() if err != nil { t.Fatalf("unable to convert sanitized resources to yaml: %v", err) } if diff := cmp.Diff(string(sYaml), tc.expected); diff != "" { t.Errorf("unexpected sanitized resources: (-got +want)%v", diff) } }) } } func Test_unMarshallKustomization(t *testing.T) { tests := []struct { name string localKsFile string wantErr bool errString string }{ { name: "valid kustomization", localKsFile: "testdata/local-kustomization/valid.yaml", }, { name: "Multi-doc yaml containing kustomization and other resources", localKsFile: "testdata/local-kustomization/multi-doc-valid.yaml", }, { name: "no namespace", localKsFile: "testdata/local-kustomization/no-ns.yaml", }, { name: "kustomization with a different name", localKsFile: "testdata/local-kustomization/different-name.yaml", wantErr: true, errString: "failed find kustomization with name", }, { name: "yaml containing other resource with same name as kustomization", localKsFile: "testdata/local-kustomization/invalid-resource.yaml", wantErr: true, errString: "failed find kustomization with name", }, } b := &Builder{ name: "podinfo", namespace: "flux-system", } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b.kustomizationFile = tt.localKsFile ks, err := b.unMarshallKustomization() if !tt.wantErr { if err != nil { t.Fatalf("unexpected err '%s'", err) } if ks.Name != b.name && ks.Namespace != b.namespace { t.Errorf("expected kustomization '%s/%s' to match '%s/%s'", ks.Name, ks.Namespace, b.name, b.namespace) } } else { if err == nil { t.Fatal("expected error but got nil") } if !strings.Contains(err.Error(), tt.errString) { t.Errorf("expected error '%s' to contain string '%s'", err.Error(), tt.errString) } } }) } t.Run("correct parsing of multiple documents", func(t *testing.T) { b.kustomizationFile = "testdata/local-kustomization/multi-doc-reset.yaml" ks, err := b.unMarshallKustomization() if err != nil { t.Errorf("unexpected err '%s'", err) } if len(ks.Spec.Components) > 0 { t.Errorf("previous Kustomization in file leaked into subsequent Kustomizations") } }) } func Test_ResolveKustomization(t *testing.T) { tests := []struct { name string localKsFile string liveKustomization *kustomizev1.Kustomization dryrun bool }{ { name: "valid kustomization", localKsFile: "testdata/local-kustomization/valid.yaml", }, { name: "local and live kustomization", localKsFile: "testdata/local-kustomization/valid.yaml", liveKustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Interval: metav1.Duration{Duration: time.Minute * 5}, Path: "./testdata/local-kustomization/valid.yaml", }, Status: kustomizev1.KustomizationStatus{ Conditions: []metav1.Condition{ { Type: meta.ReadyCondition, Status: metav1.ConditionTrue, }, }, Inventory: &kustomizev1.ResourceInventory{ Entries: []kustomizev1.ResourceRef{ { ID: "flux-system_podinfo_v1_service_podinfo", Version: "v1", }, }, }, }, }, }, { name: "local and live kustomization with dryrun", localKsFile: "testdata/local-kustomization/valid.yaml", liveKustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Interval: metav1.Duration{Duration: time.Minute * 5}, Path: "./testdata/local-kustomization/valid.yaml", }, Status: kustomizev1.KustomizationStatus{ Conditions: []metav1.Condition{ { Type: meta.ReadyCondition, Status: metav1.ConditionTrue, }, }, Inventory: &kustomizev1.ResourceInventory{ Entries: []kustomizev1.ResourceRef{ { ID: "flux-system_podinfo_v1_service_podinfo", Version: "v1", }, }, }, }, }, dryrun: true, }, { name: "live kustomization", liveKustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Interval: metav1.Duration{Duration: time.Minute * 5}, Path: "./testdata/local-kustomization/valid.yaml", }, Status: kustomizev1.KustomizationStatus{ Conditions: []metav1.Condition{ { Type: meta.ReadyCondition, Status: metav1.ConditionTrue, }, }, Inventory: &kustomizev1.ResourceInventory{ Entries: []kustomizev1.ResourceRef{ { ID: "flux-system_podinfo_v1_service_podinfo", Version: "v1", }, }, }, }, }, }, } b := &Builder{ name: "podinfo", namespace: "flux-system", } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b.kustomizationFile = tt.localKsFile b.dryRun = tt.dryrun ks, err := b.resolveKustomization(tt.liveKustomization) if err != nil { t.Errorf("unexpected err '%s'", err) } if !tt.dryrun { if b.kustomizationFile == "" { if cmp.Diff(ks, tt.liveKustomization) != "" { t.Errorf("expected kustomization to match live kustomization") } } else { if tt.liveKustomization != nil && cmp.Diff(ks.Status, tt.liveKustomization.Status) != "" { t.Errorf("expected kustomization status to match live kustomization status") } } } else { if ks.Status.Inventory != nil { fmt.Println(ks.Status.Inventory) t.Errorf("expected kustomization status to be nil") } } }) } } func Test_isKustomization(t *testing.T) { tests := []struct { name string expected bool object *unstructured.Unstructured }{ { name: "flux kustomization", object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "kustomize.toolkit.fluxcd.io/v1", "kind": "Kustomization", }, }, expected: true, }, { name: "other kustomization", object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "kustomize.config.k8s.io/v1beta1", "kind": "Kustomization", }, }, expected: false, }, { name: "wrong kind", object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "kustomize.toolkit.fluxcd.io/v1", "kind": "ConfigMap", }, }, expected: false, }, { name: "wrong object", object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", }, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := isKustomization(tt.object) if actual != tt.expected { t.Fatalf("got '%v', want '%v'", actual, tt.expected) } }) } } func Test_kustomizationsEqual(t *testing.T) { tests := []struct { name string kustomization1 *kustomizev1.Kustomization kustomization2 *kustomizev1.Kustomization expected bool }{ { name: "equal", kustomization1: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, }, kustomization2: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, }, expected: true, }, { name: "wrong name", kustomization1: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, }, kustomization2: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "my-app", Namespace: "flux-system", }, }, expected: false, }, { name: "wrong namespace", kustomization1: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, }, kustomization2: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "my-ns", }, }, expected: false, }, { name: "wrong name and namespace", kustomization1: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: "flux-system", }, }, kustomization2: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "my-app", Namespace: "my-ns", }, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := kustomizationsEqual(tt.kustomization1, tt.kustomization2) if actual != tt.expected { t.Fatalf("got '%v', want '%v'", actual, tt.expected) } }) } } func Test_kustomizationPath(t *testing.T) { tests := []struct { name string kustomization *kustomizev1.Kustomization expected string wantErr bool errString string }{ { name: "full repo", kustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "my-app", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Path: "my-path", SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: "GitRepository", Name: "my-repo", Namespace: "flux-system", }, }, }, expected: "path/to/local/git/my-path", }, { name: "repo without namespace", kustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "my-app", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Path: "my-path", SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: "GitRepository", Name: "my-repo", Namespace: "", }, }, }, expected: "path/to/local/git/my-path", }, { name: "repo not found", kustomization: &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: "my-app", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Path: "my-path", SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: "GitRepository", Name: "my-repo", Namespace: "my-ns", }, }, }, wantErr: true, errString: "cannot get local path", }, } b := &Builder{ name: "podinfo", namespace: "flux-system", localSources: map[string]string{ "GitRepository/flux-system/my-repo": "./path/to/local/git", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual, err := b.kustomizationPath(tt.kustomization) if !tt.wantErr { if err != nil { t.Fatalf("unexpected err '%s'", err) } if actual != tt.expected { t.Errorf("got '%v', want '%v'", actual, tt.expected) } } else { if err == nil { t.Fatal("expected error but got nil") } if !strings.Contains(err.Error(), tt.errString) { t.Errorf("expected error '%s' to contain string '%s'", err.Error(), tt.errString) } } }) } } ================================================ FILE: internal/build/diff.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package build import ( "bytes" "context" "encoding/base64" "fmt" "io" "os" "path/filepath" "sort" "strings" "github.com/gonvenience/bunt" "github.com/gonvenience/ytbx" "github.com/google/go-cmp/cmp" "github.com/homeport/dyff/pkg/dyff" "github.com/lucasb-eyer/go-colorful" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/yaml" "github.com/fluxcd/cli-utils/pkg/kstatus/polling" "github.com/fluxcd/cli-utils/pkg/object" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa/normalize" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/pkg/printers" ) func (b *Builder) Manager() (*ssa.ResourceManager, error) { statusPoller := polling.NewStatusPoller(b.client, b.restMapper, polling.Options{}) owner := ssa.Owner{ Field: controllerName, Group: controllerGroup, } return ssa.NewResourceManager(b.client, statusPoller, owner), nil } func (b *Builder) Diff() (string, bool, error) { err := b.StartSpinner() if err != nil { return "", false, err } output, createdOrDrifted, diffErr := b.diff() err = b.StopSpinner() if err != nil { return "", false, err } return output, createdOrDrifted, diffErr } func (b *Builder) diff() (string, bool, error) { output := strings.Builder{} createdOrDrifted := false objects, err := b.Build() if err != nil { return "", createdOrDrifted, err } err = normalize.UnstructuredList(objects) if err != nil { return "", createdOrDrifted, err } resourceManager, err := b.Manager() if err != nil { return "", createdOrDrifted, err } ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() var diffErrs []error // create an inventory of objects to be reconciled newInventory := newInventory() for _, obj := range objects { diffOptions := ssa.DiffOptions{ Exclusions: map[string]string{ "kustomize.toolkit.fluxcd.io/reconcile": "disabled", "kustomize.toolkit.fluxcd.io/ssa": "ignore", }, IfNotPresentSelector: map[string]string{ "kustomize.toolkit.fluxcd.io/ssa": "ifnotpresent", }, ForceSelector: map[string]string{ "kustomize.toolkit.fluxcd.io/force": "enabled", }, } change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions) if err != nil { // gather errors and continue, as we want to see all the diffs diffErrs = append(diffErrs, err) continue } if change.Action == ssa.SkippedAction { output.WriteString(writeString(fmt.Sprintf("► %s skipped\n", change.Subject), bunt.Orange)) } // if the object is a sops secret, we need to // make sure we diff only if the keys are different if obj.GetKind() == "Secret" && change.Action == ssa.ConfiguredAction { diffSopsSecret(obj, liveObject, mergedObject, change) } if change.Action == ssa.CreatedAction { output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) createdOrDrifted = true } if change.Action == ssa.ConfiguredAction { output.WriteString(bunt.Sprint(fmt.Sprintf("► %s drifted\n", change.Subject))) liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) if err != nil { return "", createdOrDrifted, err } err = diff(liveFile, mergedFile, &output) if err != nil { cleanupDir(tmpDir) return "", createdOrDrifted, err } cleanupDir(tmpDir) createdOrDrifted = true } addObjectsToInventory(newInventory, change) if b.recursive && isKustomization(obj) && change.Action != ssa.CreatedAction { k, err := toKustomization(obj) if err != nil { return "", createdOrDrifted, err } if !kustomizationsEqual(k, b.kustomization) { if k.Spec.KubeConfig != nil { output.WriteString(writeString(fmt.Sprintf("⚠️ %s skipped: diff not supported for remote clusters\n", ssautil.FmtUnstructured(obj)), bunt.Orange)) } else { subOutput, subCreatedOrDrifted, err := b.kustomizationDiff(k) if err != nil { diffErrs = append(diffErrs, err) } if subCreatedOrDrifted { createdOrDrifted = true output.WriteString(bunt.Sprint(fmt.Sprintf("📁 %s changed\n", ssautil.FmtUnstructured(obj)))) output.WriteString(subOutput) } } // finished with Kustomization diff if b.spinner != nil { b.spinner.Message(spinnerDryRunMessage) } } } } if b.spinner != nil { b.spinner.Message("processing inventory") } if b.kustomization.Spec.Prune && len(diffErrs) == 0 { oldStatus := b.kustomization.Status.DeepCopy() if oldStatus.Inventory != nil { staleObjects, err := diffInventory(oldStatus.Inventory, newInventory) if err != nil { return "", createdOrDrifted, err } if len(staleObjects) > 0 { createdOrDrifted = true } for _, object := range staleObjects { output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssautil.FmtUnstructured(object)), bunt.OrangeRed)) } } } return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs))) } func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) { if b.spinner != nil { b.spinner.Message(fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name)) } sourceRef := kustomization.Spec.SourceRef.DeepCopy() if sourceRef.Namespace == "" { sourceRef.Namespace = kustomization.Namespace } sourceKey := sourceRef.String() localPath, ok := b.localSources[sourceKey] if !ok { return "", false, fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, kustomization.Name) } resourcesPath := filepath.Join(localPath, kustomization.Spec.Path) subBuilder, err := NewBuilder(kustomization.Name, resourcesPath, // use same client and spinner withClientConfigFrom(b), withSpinnerFrom(b), WithTimeout(b.timeout), WithNamespace(kustomization.Namespace), WithIgnore(b.ignore), WithStrictSubstitute(b.strictSubst), WithRecursive(b.recursive), WithLocalSources(b.localSources), WithSingleKustomization(), ) if err != nil { return "", false, err } return subBuilder.diff() } func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { tmpDir, err := os.MkdirTemp("", "") if err != nil { return "", "", "", err } liveYAML, _ := yaml.Marshal(liveObject) liveFile := filepath.Join(tmpDir, "live.yaml") if err := os.WriteFile(liveFile, liveYAML, 0o600); err != nil { return "", "", "", err } mergedYAML, _ := yaml.Marshal(mergedObject) mergedFile := filepath.Join(tmpDir, "merged.yaml") if err := os.WriteFile(mergedFile, mergedYAML, 0o600); err != nil { return "", "", "", err } return liveFile, mergedFile, tmpDir, nil } func writeString(t string, color colorful.Color) string { return bunt.Style( t, bunt.EachLine(), bunt.Foreground(color), ) } func cleanupDir(dir string) error { return os.RemoveAll(dir) } func diff(liveFile, mergedFile string, output io.Writer) error { from, to, err := ytbx.LoadFiles(liveFile, mergedFile) if err != nil { return fmt.Errorf("failed to load input files: %w", err) } report, err := dyff.CompareInputFiles(from, to, dyff.IgnoreOrderChanges(false), dyff.KubernetesEntityDetection(true), ) if err != nil { return fmt.Errorf("failed to compare input files: %w", err) } printer := printers.NewDyffPrinter() printer.Print(output, report) return nil } func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { // get both data and stringdata maps data := obj.Object[dataField] if m, ok := data.(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 = ssa.UnchangedAction liveKeys, mergedKeys := sopsComparableByKeys(liveObject), sopsComparableByKeys(mergedObject) if cmp.Diff(liveKeys, mergedKeys) != "" { change.Action = ssa.ConfiguredAction } } } } } func sopsComparableByKeys(object *unstructured.Unstructured) []string { m := object.Object[dataField].(map[string]interface{}) keys := make([]string, len(m)) i := 0 for k := range m { // make sure we can compare only on keys m[k] = "*****" keys[i] = k i++ } object.Object[dataField] = m sort.Strings(keys) return keys } // diffInventory returns the slice of objects that do not exist in the target inventory. func diffInventory(inv *kustomizev1.ResourceInventory, target *kustomizev1.ResourceInventory) ([]*unstructured.Unstructured, error) { versionOf := func(i *kustomizev1.ResourceInventory, objMetadata object.ObjMetadata) string { for _, entry := range i.Entries { if entry.ID == objMetadata.String() { return entry.Version } } return "" } objects := make([]*unstructured.Unstructured, 0) aList, err := listMetaInInventory(inv) if err != nil { return nil, err } bList, err := listMetaInInventory(target) if err != nil { return nil, err } list := aList.Diff(bList) if len(list) == 0 { return objects, nil } for _, metadata := range list { u := &unstructured.Unstructured{} u.SetGroupVersionKind(schema.GroupVersionKind{ Group: metadata.GroupKind.Group, Kind: metadata.GroupKind.Kind, Version: versionOf(inv, metadata), }) u.SetName(metadata.Name) u.SetNamespace(metadata.Namespace) objects = append(objects, u) } sort.Sort(ssa.SortableUnstructureds(objects)) return objects, nil } // listMetaInInventory returns the inventory entries as object.ObjMetadata objects. func listMetaInInventory(inv *kustomizev1.ResourceInventory) (object.ObjMetadataSet, error) { var metas []object.ObjMetadata for _, e := range inv.Entries { m, err := object.ParseObjMetadata(e.ID) if err != nil { return metas, err } metas = append(metas, m) } return metas, nil } func newInventory() *kustomizev1.ResourceInventory { return &kustomizev1.ResourceInventory{ Entries: []kustomizev1.ResourceRef{}, } } // addObjectsToInventory extracts the metadata from the given objects and adds it to the inventory. func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.ChangeSetEntry) error { if entry == nil { return nil } inv.Entries = append(inv.Entries, kustomizev1.ResourceRef{ ID: entry.ObjMetadata.String(), Version: entry.GroupVersion, }) return nil } ================================================ FILE: internal/build/testdata/local-kustomization/different-name.yaml ================================================ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infra namespace: flux-system spec: path: "./clusters/test-build" ================================================ FILE: internal/build/testdata/local-kustomization/invalid-resource.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: podinfo ================================================ FILE: internal/build/testdata/local-kustomization/multi-doc-reset.yaml ================================================ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: first namespace: flux-system spec: path: "./k8s/first" components: - foo --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: flux-system spec: path: "./k8s/second" --- ================================================ FILE: internal/build/testdata/local-kustomization/multi-doc-valid.yaml ================================================ --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infra-namespace namespace: flux-system labels: component.kutara.io/part-of: definitions spec: path: "./k8s/base/infra" prune: true --- --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo namespace: flux-system spec: interval: 30s ref: branch: master url: https://github.com/stefanprodan/podinfo --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: flux-system spec: path: "./clusters/test-build" --- ================================================ FILE: internal/build/testdata/local-kustomization/no-ns.yaml ================================================ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo spec: path: "./clusters/test-build" ================================================ FILE: internal/build/testdata/local-kustomization/valid.yaml ================================================ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: flux-system spec: path: "./clusters/test-build" ================================================ FILE: internal/flags/crds.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedCRDsPolicies = []string{ string(helmv2.Skip), string(helmv2.Create), string(helmv2.CreateReplace), } type CRDsPolicy string func (a *CRDsPolicy) String() string { return string(*a) } func (a *CRDsPolicy) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no upgrade CRDs policy given, must be one of: %s", strings.Join(supportedCRDsPolicies, ", ")) } if !utils.ContainsItemString(supportedCRDsPolicies, str) { return fmt.Errorf("unsupported upgrade CRDs policy '%s', must be one of: %s", str, strings.Join(supportedCRDsPolicies, ", ")) } *a = CRDsPolicy(str) return nil } func (a *CRDsPolicy) Type() string { return strings.Join(supportedCRDsPolicies, "|") } func (a *CRDsPolicy) Description() string { return "upgrade CRDs policy" } ================================================ FILE: internal/flags/crds_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestCRDsPolicy_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "CreateReplace", "CreateReplace", false}, {"unsupported", "createreplace", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var a CRDsPolicy if err := a.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := a.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/decryption_provider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedDecryptionProviders = []string{"sops"} type DecryptionProvider string func (d *DecryptionProvider) String() string { return string(*d) } func (d *DecryptionProvider) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no decryption provider given, must be one of: %s", strings.Join(supportedDecryptionProviders, ", ")) } if !utils.ContainsItemString(supportedDecryptionProviders, str) { return fmt.Errorf("unsupported decryption provider '%s', must be one of: %s", str, strings.Join(supportedDecryptionProviders, ", ")) } *d = DecryptionProvider(str) return nil } func (d *DecryptionProvider) Type() string { return "decryptionProvider" } func (d *DecryptionProvider) Description() string { return fmt.Sprintf("decryption provider, available options are: (%s)", strings.Join(supportedDecryptionProviders, ", ")) } ================================================ FILE: internal/flags/decryption_provider_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestDecryptionProvider_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "sops", "sops", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var p DecryptionProvider if err := p.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := p.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/ecdsa_curve.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "crypto/elliptic" "fmt" "sort" "strings" ) type ECDSACurve struct { elliptic.Curve } var supportedECDSACurves = map[string]elliptic.Curve{ "p256": elliptic.P256(), "p384": elliptic.P384(), "p521": elliptic.P521(), } func (c *ECDSACurve) String() string { if c.Curve == nil { return "" } return strings.ToLower(strings.Replace(c.Curve.Params().Name, "-", "", 1)) } func (c *ECDSACurve) Set(str string) error { if v, ok := supportedECDSACurves[str]; ok { *c = ECDSACurve{v} return nil } return fmt.Errorf("unsupported curve '%s', must be one of: %s", str, strings.Join(ecdsaCurves(), ", ")) } func (c *ECDSACurve) Type() string { keys := make([]string, 0, len(supportedECDSACurves)) for k := range supportedECDSACurves { keys = append(keys, k) } sort.Strings(keys) return strings.Join(keys, "|") } func (c *ECDSACurve) Description() string { return "SSH ECDSA public key curve" } func ecdsaCurves() []string { keys := make([]string, 0, len(supportedECDSACurves)) for k := range supportedECDSACurves { keys = append(keys, k) } sort.Strings(keys) return keys } ================================================ FILE: internal/flags/ecdsa_curve_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestECDSACurve_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "p256", "p256", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var c ECDSACurve if err := c.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := c.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/gitlab_visibility.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/go-git-providers/validation" ) var supportedGitLabVisibilities = map[gitprovider.RepositoryVisibility]struct{}{ gitprovider.RepositoryVisibilityPublic: {}, gitprovider.RepositoryVisibilityInternal: {}, gitprovider.RepositoryVisibilityPrivate: {}, } // ValidateRepositoryVisibility validates a given RepositoryVisibility. func ValidateRepositoryVisibility(r gitprovider.RepositoryVisibility) error { _, ok := supportedGitLabVisibilities[r] if !ok { return validation.ErrFieldEnumInvalid } return nil } type GitLabVisibility gitprovider.RepositoryVisibility func (d *GitLabVisibility) String() string { return string(*d) } func (d *GitLabVisibility) Set(str string) error { if strings.TrimSpace(str) == "" { str = string(gitprovider.RepositoryVisibilityPrivate) } var visibility = gitprovider.RepositoryVisibility(str) if ValidateRepositoryVisibility(visibility) != nil { return fmt.Errorf("unsupported visibility '%s'", str) } *d = GitLabVisibility(visibility) return nil } func (d *GitLabVisibility) Type() string { keys := make([]string, 0, len(supportedGitLabVisibilities)) for v := range supportedGitLabVisibilities { keys = append(keys, string(v)) } return strings.Join(keys, "|") } func (d *GitLabVisibility) Description() string { return "specifies the visibility of the repository" } ================================================ FILE: internal/flags/gitlab_visibility_test.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestGitLabVisibility_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"private", "private", "private", false}, {"internal", "internal", "internal", false}, {"public", "public", "public", false}, {"unsupported", "unsupported", "", true}, {"default", "", "private", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var p GitLabVisibility if err := p.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := p.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/helm_chart_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedHelmChartSourceKinds = []string{sourcev1.HelmRepositoryKind, sourcev1.GitRepositoryKind, sourcev1.BucketKind} type HelmChartSource struct { Kind string Name string Namespace string } func (s *HelmChartSource) String() string { if s.Name == "" { return "" } return fmt.Sprintf("%s/%s", s.Kind, s.Name) } func (s *HelmChartSource) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no helm chart source given, please specify %s", s.Description()) } sourceKind, sourceName, sourceNamespace := utils.ParseObjectKindNameNamespace(str) if sourceKind == "" || sourceName == "" { return fmt.Errorf("invalid helm chart source '%s', must be in format /", str) } cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmChartSourceKinds, sourceKind) if !ok { return fmt.Errorf("source kind '%s' is not supported, must be one of: %s", sourceKind, strings.Join(supportedHelmChartSourceKinds, ", ")) } s.Kind = cleanSourceKind s.Name = sourceName s.Namespace = sourceNamespace return nil } func (s *HelmChartSource) Type() string { return "string" } func (s *HelmChartSource) Description() string { return fmt.Sprintf( "source that contains the chart in the format '/.', "+ "where kind must be one of: (%s)", strings.Join(supportedHelmChartSourceKinds, ", "), ) } ================================================ FILE: internal/flags/helm_chart_source_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "testing" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestHelmChartSource_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", fmt.Sprintf("%s/foo", sourcev1.HelmRepositoryKind), fmt.Sprintf("%s/foo", sourcev1.HelmRepositoryKind), false}, {"lower case kind", "helmrepository/foo", fmt.Sprintf("%s/foo", sourcev1.HelmRepositoryKind), false}, {"unsupported", "Unsupported/kind", "", true}, {"invalid format", sourcev1.HelmRepositoryKind, "", true}, {"missing name", fmt.Sprintf("%s/", sourcev1.HelmRepositoryKind), "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var s HelmChartSource if err := s.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := s.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/kustomization_source.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedKustomizationSourceKinds = []string{sourcev1.OCIRepositoryKind, sourcev1.GitRepositoryKind, sourcev1.BucketKind} type KustomizationSource struct { Kind string Name string Namespace string } func (s *KustomizationSource) String() string { if s.Name == "" { return "" } return fmt.Sprintf("%s/%s", s.Kind, s.Name) } func (s *KustomizationSource) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no Kustomization source given, please specify %s", s.Description()) } sourceKind, sourceName, sourceNamespace := utils.ParseObjectKindNameNamespace(str) if sourceName == "" { return fmt.Errorf("no name given for source of kind '%s'", sourceKind) } if sourceKind == "" { if utils.ContainsItemString(supportedKustomizationSourceKinds, sourceName) { return fmt.Errorf("no kind specified for source '%s'", sourceName) } sourceKind = sourcev1.GitRepositoryKind } cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedKustomizationSourceKinds, sourceKind) if !ok { return fmt.Errorf("source kind '%s' is not supported, must be one of: %s", sourceKind, strings.Join(supportedKustomizationSourceKinds, ", ")) } s.Kind = cleanSourceKind s.Name = sourceName s.Namespace = sourceNamespace return nil } func (s *KustomizationSource) Type() string { return "string" } func (s *KustomizationSource) Description() string { return fmt.Sprintf( "source that contains the Kubernetes manifests in the format '[/].', "+ "where kind must be one of: (%s), if kind is not specified it defaults to GitRepository", strings.Join(supportedKustomizationSourceKinds, ", "), ) } ================================================ FILE: internal/flags/kustomization_source_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "testing" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestKustomizationSource_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", fmt.Sprintf("%s/foo", sourcev1.GitRepositoryKind), fmt.Sprintf("%s/foo", sourcev1.GitRepositoryKind), false}, {"default kind", "foo", fmt.Sprintf("%s/foo", sourcev1.GitRepositoryKind), false}, {"lower case kind", "gitrepository/foo", fmt.Sprintf("%s/foo", sourcev1.GitRepositoryKind), false}, {"unsupported", "Unsupported/kind", "", true}, {"missing name", fmt.Sprintf("%s/", sourcev1.GitRepositoryKind), "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var s KustomizationSource if err := s.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := s.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/local_helm_chart_source.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" ) type LocalHelmChartSource struct { Kind string Name string } func (s *LocalHelmChartSource) String() string { if s.Name == "" { return "" } return fmt.Sprintf("%s/%s", s.Kind, s.Name) } func (s *LocalHelmChartSource) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no helm chart source given, please specify %s", s.Description()) } sourceKind, sourceName := utils.ParseObjectKindName(str) if sourceKind == "" || sourceName == "" { return fmt.Errorf("invalid helm chart source '%s', must be in format /", str) } cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmChartSourceKinds, sourceKind) if !ok { return fmt.Errorf("source kind '%s' is not supported, must be one of: %s", sourceKind, strings.Join(supportedHelmChartSourceKinds, ", ")) } s.Kind = cleanSourceKind s.Name = sourceName return nil } func (s *LocalHelmChartSource) Type() string { return "helmChartSource" } func (s *LocalHelmChartSource) Description() string { return fmt.Sprintf( "source that contains the chart in the format '/', "+ "where kind must be one of: (%s)", strings.Join(supportedHelmChartSourceKinds, ", "), ) } ================================================ FILE: internal/flags/log_level.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedLogLevels = []string{"debug", "info", "error"} type LogLevel string func (l *LogLevel) String() string { return string(*l) } func (l *LogLevel) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no log level given, must be one of: %s", strings.Join(supportedLogLevels, ", ")) } if !utils.ContainsItemString(supportedLogLevels, str) { return fmt.Errorf("unsupported log level '%s', must be one of: %s", str, strings.Join(supportedLogLevels, ", ")) } *l = LogLevel(str) return nil } func (l *LogLevel) Type() string { return strings.Join(supportedLogLevels, "|") } func (l *LogLevel) Description() string { return "log level" } ================================================ FILE: internal/flags/log_level_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestLogLevel_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "info", "info", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var l LogLevel if err := l.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := l.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/public_key_algorithm.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" ) var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa", "ed25519"} type PublicKeyAlgorithm string func (a *PublicKeyAlgorithm) String() string { return string(*a) } func (a *PublicKeyAlgorithm) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no public key algorithm given, must be one of: %s", strings.Join(supportedPublicKeyAlgorithms, ", ")) } for _, v := range supportedPublicKeyAlgorithms { if str == v { *a = PublicKeyAlgorithm(str) return nil } } return fmt.Errorf("unsupported public key algorithm '%s', must be one of: %s", str, strings.Join(supportedPublicKeyAlgorithms, ", ")) } func (a *PublicKeyAlgorithm) Type() string { return strings.Join(supportedPublicKeyAlgorithms, "|") } func (a *PublicKeyAlgorithm) Description() string { return "SSH public key algorithm" } ================================================ FILE: internal/flags/public_key_algorithm_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestPublicKeyAlgorithm_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "rsa", "rsa", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var a PublicKeyAlgorithm if err := a.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := a.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/rsa_key_bits.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strconv" "strings" ) var defaultRSAKeyBits = 2048 type RSAKeyBits int func (b *RSAKeyBits) String() string { return strconv.Itoa(int(*b)) } func (b *RSAKeyBits) Set(str string) error { if strings.TrimSpace(str) == "" { *b = RSAKeyBits(defaultRSAKeyBits) return nil } bits, err := strconv.Atoi(str) if err != nil { return err } if bits < 1024 { return fmt.Errorf("RSA key bit size must be at least 1024") } if bits == 0 || bits%8 != 0 { return fmt.Errorf("RSA key bit size must be a multiples of 8") } *b = RSAKeyBits(bits) return nil } func (b *RSAKeyBits) Type() string { return "rsaKeyBits" } func (b *RSAKeyBits) Description() string { return "SSH RSA public key bit size (multiplies of 8, min 1024)" } ================================================ FILE: internal/flags/rsa_key_bits_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestRSAKeyBits_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "4096", "4096", false}, {"empty (default)", "", "2048", false}, {"unsupported", "512", "0", true}, {"unsupported", "1025", "0", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var b RSAKeyBits if err := b.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := b.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/safe_relative_path.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "path/filepath" "strings" securejoin "github.com/cyphar/filepath-securejoin" ) type SafeRelativePath string func (p *SafeRelativePath) String() string { return string(*p) } func (p *SafeRelativePath) ToSlash() string { return filepath.ToSlash(p.String()) } func (p *SafeRelativePath) Set(str string) error { // The result of secure joining on a relative base dir is a flattened relative path. cleanP, err := securejoin.SecureJoin("./", strings.TrimSpace(str)) if err != nil { return fmt.Errorf("invalid relative path '%s': %w", cleanP, err) } // NB: required, as a secure join of "./" will result in "." if cleanP == "." { cleanP = "" } cleanP = fmt.Sprintf("./%s", cleanP) *p = SafeRelativePath(cleanP) return nil } func (p *SafeRelativePath) Type() string { return "safeRelativePath" } func (p *SafeRelativePath) Description() string { return "secure relative path" } ================================================ FILE: internal/flags/safe_relative_path_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestRelativePath_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"relative path", "./foo", "./foo", false}, {"relative path", "foo", "./foo", false}, {"traversing relative path", "./foo/../bar", "./bar", false}, {"absolute path", "/foo", "./foo", false}, {"traversing absolute path", "/foo/../bar", "./bar", false}, {"traversing overflowing absolute path", "/foo/../../../bar", "./bar", false}, {"empty", "", "./", false}, {"relative empty path", "./", "./", false}, {"double relative empty path", "././", "./", false}, {"dot path", ".foo", "./.foo", false}, {"relative dot path", "./.foo", "./.foo", false}, {"current directory", ".", "./", false}, {"parent directory", "..", "./", false}, {"parent directory more qualified", "./..", "./", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var p SafeRelativePath if err := p.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := p.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/source_bucket_provider.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedSourceBucketProviders = []string{ sourcev1.BucketProviderGeneric, sourcev1.BucketProviderAmazon, sourcev1.BucketProviderAzure, sourcev1.BucketProviderGoogle, } type SourceBucketProvider string func (p *SourceBucketProvider) String() string { return string(*p) } func (p *SourceBucketProvider) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no source bucket provider given, please specify %s", p.Description()) } if !utils.ContainsItemString(supportedSourceBucketProviders, str) { return fmt.Errorf("source bucket provider '%s' is not supported, must be one of: %v", str, strings.Join(supportedSourceBucketProviders, ", ")) } *p = SourceBucketProvider(str) return nil } func (p *SourceBucketProvider) Type() string { return strings.Join(supportedSourceBucketProviders, "|") } func (p *SourceBucketProvider) Description() string { return "the S3 compatible storage provider name" } ================================================ FILE: internal/flags/source_bucket_provider_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestSourceBucketProvider_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", sourcev1.BucketProviderGeneric, sourcev1.BucketProviderGeneric, false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var s SourceBucketProvider if err := s.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := s.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/flags/source_git_provider.go ================================================ /* Copyright 2024 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var supportedSourceGitProviders = []string{ sourcev1.GitProviderGeneric, sourcev1.GitProviderAzure, sourcev1.GitProviderGitHub, } type SourceGitProvider string func (p *SourceGitProvider) String() string { return string(*p) } func (p *SourceGitProvider) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no source Git provider given, please specify %s", p.Description()) } if !utils.ContainsItemString(supportedSourceGitProviders, str) { return fmt.Errorf("source Git provider '%s' is not supported, must be one of: %v", str, p.Type()) } *p = SourceGitProvider(str) return nil } func (p *SourceGitProvider) Type() string { return strings.Join(supportedSourceGitProviders, "|") } func (p *SourceGitProvider) Description() string { return "the Git provider name" } ================================================ FILE: internal/flags/source_oci_provider.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedSourceOCIProviders = []string{ sourcev1.GenericOCIProvider, sourcev1.AmazonOCIProvider, sourcev1.AzureOCIProvider, sourcev1.GoogleOCIProvider, } type SourceOCIProvider string func (p *SourceOCIProvider) String() string { return string(*p) } func (p *SourceOCIProvider) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no source OCI provider given, please specify %s", p.Description()) } if !utils.ContainsItemString(supportedSourceOCIProviders, str) { return fmt.Errorf("source OCI provider '%s' is not supported, must be one of: %v", str, strings.Join(supportedSourceOCIProviders, ", ")) } *p = SourceOCIProvider(str) return nil } func (p *SourceOCIProvider) Type() string { return strings.Join(supportedSourceOCIProviders, "|") } func (p *SourceOCIProvider) Description() string { return "the OCI provider name" } ================================================ FILE: internal/flags/source_oci_verify_provider.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "fmt" "strings" "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedSourceOCIVerifyProviders = []string{ "cosign", } type SourceOCIVerifyProvider string func (p *SourceOCIVerifyProvider) String() string { return string(*p) } func (p *SourceOCIVerifyProvider) Set(str string) error { if strings.TrimSpace(str) == "" { return fmt.Errorf("no source OCI verify provider given, please specify %s", p.Description()) } if !utils.ContainsItemString(supportedSourceOCIVerifyProviders, str) { return fmt.Errorf("source OCI verify provider '%s' is not supported, must be one of: %v", str, strings.Join(supportedSourceOCIVerifyProviders, ", ")) } *p = SourceOCIVerifyProvider(str) return nil } func (p *SourceOCIVerifyProvider) Type() string { return "sourceOCIVerifyProvider" } func (p *SourceOCIVerifyProvider) Description() string { return fmt.Sprintf( "the OCI verify provider name to use for signature verification, available options are: (%s)", strings.Join(supportedSourceOCIVerifyProviders, ", "), ) } ================================================ FILE: internal/flags/source_oci_verify_provider_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package flags import ( "testing" ) func TestSourceOCIVerifyProvider_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ {"supported", "cosign", "cosign", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var s SourceOCIVerifyProvider if err := s.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } if str := s.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) } } ================================================ FILE: internal/tree/tree.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Derived work from https://github.com/d6o/GoTree Copyright (c) 2017 Diego Siqueira */ package tree import ( "strings" "github.com/fluxcd/cli-utils/pkg/object" ssautil "github.com/fluxcd/pkg/ssa/utils" ) const ( newLine = "\n" emptySpace = " " middleItem = "├── " continueItem = "│ " lastItem = "└── " ) type ( objMetadataTree struct { Resource object.ObjMetadata `json:"resource"` ResourceTree []ObjMetadataTree `json:"resources,omitempty"` } ObjMetadataTree interface { Add(objMetadata object.ObjMetadata) ObjMetadataTree AddTree(tree ObjMetadataTree) Items() []ObjMetadataTree Text() string Print() string } printer struct { } Printer interface { Print(ObjMetadataTree) string } ) func New(objMetadata object.ObjMetadata) ObjMetadataTree { return &objMetadataTree{ Resource: objMetadata, ResourceTree: []ObjMetadataTree{}, } } func (t *objMetadataTree) Add(objMetadata object.ObjMetadata) ObjMetadataTree { n := New(objMetadata) t.ResourceTree = append(t.ResourceTree, n) return n } func (t *objMetadataTree) AddTree(tree ObjMetadataTree) { t.ResourceTree = append(t.ResourceTree, tree) } func (t *objMetadataTree) Text() string { return ssautil.FmtObjMetadata(t.Resource) } func (t *objMetadataTree) Items() []ObjMetadataTree { return t.ResourceTree } func (t *objMetadataTree) Print() string { return newPrinter().Print(t) } func newPrinter() Printer { return &printer{} } func (p *printer) Print(t ObjMetadataTree) string { return t.Text() + newLine + p.printItems(t.Items(), []bool{}) } func (p *printer) printText(text string, spaces []bool, last bool) string { var result string for _, space := range spaces { if space { result += emptySpace } else { result += continueItem } } indicator := middleItem if last { indicator = lastItem } var out string lines := strings.Split(text, "\n") for i := range lines { text := lines[i] if i == 0 { out += result + indicator + text + newLine continue } if last { indicator = emptySpace } else { indicator = continueItem } out += result + indicator + text + newLine } return out } func (p *printer) printItems(t []ObjMetadataTree, spaces []bool) string { var result string for i, f := range t { last := i == len(t)-1 result += p.printText(f.Text(), spaces, last) if len(f.Items()) > 0 { spacesChild := append(spaces, last) result += p.printItems(f.Items(), spacesChild) } } return result } ================================================ FILE: internal/utils/apply.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "bufio" "bytes" "context" "fmt" "os" "path/filepath" "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/konfig" "github.com/fluxcd/cli-utils/pkg/kstatus/polling" runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa/normalize" ssautil "github.com/fluxcd/pkg/ssa/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" ) // Apply is the equivalent of 'kubectl apply --server-side -f'. // If the given manifest is a kustomization.yaml, then apply performs the equivalent of 'kubectl apply --server-side -k'. func Apply(ctx context.Context, rcg genericclioptions.RESTClientGetter, opts *runclient.Options, root, manifestPath string) (string, error) { objs, err := readObjects(root, manifestPath) if err != nil { return "", err } if len(objs) == 0 { return "", fmt.Errorf("no Kubernetes objects found at: %s", manifestPath) } if err := normalize.UnstructuredList(objs); err != nil { return "", err } changeSet := ssa.NewChangeSet() // contains only CRDs and Namespaces var stageOne []*unstructured.Unstructured // contains all objects except for CRDs and Namespaces var stageTwo []*unstructured.Unstructured for _, u := range objs { if ssautil.IsClusterDefinition(u) { stageOne = append(stageOne, u) } else { stageTwo = append(stageTwo, u) } } if len(stageOne) > 0 { cs, err := applySet(ctx, rcg, opts, stageOne) if err != nil { return "", err } changeSet.Append(cs.Entries) } if len(changeSet.Entries) > 0 { if err := waitForSet(rcg, opts, changeSet); err != nil { return "", err } } if len(stageTwo) > 0 { cs, err := applySet(ctx, rcg, opts, stageTwo) if err != nil { return "", err } changeSet.Append(cs.Entries) } return changeSet.String(), nil } func readObjects(root, manifestPath string) ([]*unstructured.Unstructured, error) { fi, err := os.Lstat(manifestPath) if err != nil { return nil, err } if fi.IsDir() || !fi.Mode().IsRegular() { return nil, fmt.Errorf("expected %q to be a file", manifestPath) } if isRecognizedKustomizationFile(manifestPath) { resources, err := kustomization.BuildWithRoot(root, filepath.Dir(manifestPath)) if err != nil { return nil, err } return ssautil.ReadObjects(bytes.NewReader(resources)) } ms, err := os.Open(manifestPath) if err != nil { return nil, err } defer ms.Close() return ssautil.ReadObjects(bufio.NewReader(ms)) } func newManager(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (*ssa.ResourceManager, error) { cfg, err := KubeConfig(rcg, opts) if err != nil { return nil, err } restMapper, err := rcg.ToRESTMapper() if err != nil { return nil, err } kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: NewScheme()}) if err != nil { return nil, err } kubePoller := polling.NewStatusPoller(kubeClient, restMapper, polling.Options{}) return ssa.NewResourceManager(kubeClient, kubePoller, ssa.Owner{ Field: "flux", Group: "fluxcd.io", }), nil } func applySet(ctx context.Context, rcg genericclioptions.RESTClientGetter, opts *runclient.Options, objects []*unstructured.Unstructured) (*ssa.ChangeSet, error) { man, err := newManager(rcg, opts) if err != nil { return nil, err } return man.ApplyAll(ctx, objects, ssa.DefaultApplyOptions()) } func waitForSet(rcg genericclioptions.RESTClientGetter, opts *runclient.Options, changeSet *ssa.ChangeSet) error { man, err := newManager(rcg, opts) if err != nil { return err } return man.WaitForSet(changeSet.ToObjMetadataSet(), ssa.WaitOptions{Interval: 2 * time.Second, Timeout: time.Minute}) } func isRecognizedKustomizationFile(path string) bool { base := filepath.Base(path) for _, v := range konfig.RecognizedKustomizationFileNames() { if base == v { return true } } return false } ================================================ FILE: internal/utils/hex.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "regexp" "strings" ) // hexRegexp matches any hexadecimal notation between 40 and 128 characters. var hexRegexp = regexp.MustCompile(`\b[a-f0-9]{40,128}\b`) // TruncateHex will replace any hexadecimal notation between 40 and 128 // characters (SHA-1 up to SHA-512) within the given string with a truncated // version of 8 characters. func TruncateHex(str string) string { if str == "" { return "" } hits := hexRegexp.FindAllString(str, -1) for _, v := range hits { str = strings.Replace(str, v, string([]rune(v)[:8]), -1) } return str } ================================================ FILE: internal/utils/hex_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "testing" . "github.com/onsi/gomega" ) func TestTruncateHex(t *testing.T) { tests := []struct { name string str string want string }{ { name: "SHA1 hash", str: "16cfcc0b9066b3234dda29927ac1c19860d9663f", want: "16cfcc0b", }, { name: "SHA256 hash", str: "c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", want: "c2448c95", }, { name: "BLAKE3 hash", str: "d7b332559f4e57c01dcbe24e53346b4e47696fd2a07f39639a4017a8c3a1d045", want: "d7b33255", }, { name: "SHA512 hash", str: "dd81564b7e1e1d5986b166c21963d602f47f8610bf2a6ebbfd2f9c1e5ef05ef134f07e587383cbc049325c43e0e6817b5a282a74c0d569a5e057118484989781", want: "dd81564b", }, { name: "part of digest", str: "sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", want: "sha256:c2448c95", }, { name: "part of revision with digest", str: "tag@sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", want: "tag@sha256:c2448c95", }, { name: "legacy revision with hash", str: "HEAD/16cfcc0b9066b3234dda29927ac1c19860d9663f", want: "HEAD/16cfcc0b", }, { name: "hex exceeding max length", str: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", want: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { name: "hex under min length", str: "ffffffffffffff", want: "ffffffffffffff", }, { name: "within string", str: "this is a lengthy string with a hash 16cfcc0b9066b3234dda29927ac1c19860d9663f in it", want: "this is a lengthy string with a hash 16cfcc0b in it", }, { name: "within string (quoted)", str: "this is a lengthy string with a hash \"c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0\" in it", want: "this is a lengthy string with a hash \"c2448c95\" in it", }, { name: "within string (single quoted)", str: "this is a lengthy string with a hash 'sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0' in it", want: "this is a lengthy string with a hash 'sha256:c2448c95' in it", }, { name: "arbitrary string", str: "which should not be modified", want: "which should not be modified", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) g.Expect(TruncateHex(tt.str)).To(Equal(tt.want)) }) } } ================================================ FILE: internal/utils/testdata/components-with-crds.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: flux-system --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: creationTimestamp: null name: alerts.notification.toolkit.fluxcd.io spec: group: notification.toolkit.fluxcd.io names: kind: Alert listKind: AlertList plural: alerts singular: alert scope: Namespaced versions: - name: v1beta1 served: true storage: true subresources: status: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: creationTimestamp: null name: buckets.source.toolkit.fluxcd.io spec: group: source.toolkit.fluxcd.io names: kind: Bucket listKind: BucketList plural: buckets singular: bucket scope: Namespaced versions: - name: v1beta1 served: true storage: true subresources: status: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] --- apiVersion: v1 kind: ServiceAccount metadata: name: kustomize-controller namespace: flux-system --- apiVersion: v1 kind: ServiceAccount metadata: name: notification-controller namespace: flux-system ================================================ FILE: internal/utils/testdata/components-without-crds.yaml ================================================ --- apiVersion: v1 kind: Namespace metadata: name: flux-system --- apiVersion: v1 kind: ServiceAccount metadata: name: kustomize-controller namespace: flux-system --- apiVersion: v1 kind: ServiceAccount metadata: name: notification-controller namespace: flux-system ================================================ FILE: internal/utils/utils.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "bytes" "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" sigyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" helmv2 "github.com/fluxcd/helm-controller/api/v2" imageautov1 "github.com/fluxcd/image-automation-controller/api/v1" imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/version" sourcev1 "github.com/fluxcd/source-controller/api/v1" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) type ExecMode string const ( ModeOS ExecMode = "os.stderr|stdout" ModeStderrOS ExecMode = "os.stderr" ModeCapture ExecMode = "capture.stderr|stdout" ) func ExecKubectlCommand(ctx context.Context, mode ExecMode, kubeConfigPath string, kubeContext string, args ...string) (string, error) { var stdoutBuf, stderrBuf bytes.Buffer if kubeConfigPath != "" && len(filepath.SplitList(kubeConfigPath)) == 1 { args = append(args, "--kubeconfig="+kubeConfigPath) } if kubeContext != "" { args = append(args, "--context="+kubeContext) } c := exec.CommandContext(ctx, "kubectl", args...) if mode == ModeStderrOS { c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) } if mode == ModeOS { c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) } if mode == ModeStderrOS || mode == ModeOS { if err := c.Run(); err != nil { return "", err } else { return "", nil } } if mode == ModeCapture { c.Stdout = &stdoutBuf c.Stderr = &stderrBuf if err := c.Run(); err != nil { return stderrBuf.String(), err } else { return stdoutBuf.String(), nil } } return "", nil } func KubeConfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (*rest.Config, error) { cfg, err := rcg.ToRESTConfig() if err != nil { return nil, fmt.Errorf("kubernetes configuration load failed: %w", err) } // avoid throttling request when some Flux CRDs are not registered cfg.QPS = opts.QPS cfg.Burst = opts.Burst return cfg, nil } // Create the Scheme, methods for serializing and deserializing API objects // which can be shared by tests. func NewScheme() *apiruntime.Scheme { scheme := apiruntime.NewScheme() _ = apiextensionsv1.AddToScheme(scheme) _ = corev1.AddToScheme(scheme) _ = rbacv1.AddToScheme(scheme) _ = appsv1.AddToScheme(scheme) _ = networkingv1.AddToScheme(scheme) _ = sourcev1.AddToScheme(scheme) _ = kustomizev1.AddToScheme(scheme) _ = helmv2.AddToScheme(scheme) _ = notificationv1.AddToScheme(scheme) _ = notificationv1b3.AddToScheme(scheme) _ = imagereflectv1.AddToScheme(scheme) _ = imageautov1.AddToScheme(scheme) _ = swapi.AddToScheme(scheme) return scheme } func KubeClient(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (client.WithWatch, error) { cfg, err := rcg.ToRESTConfig() if err != nil { return nil, err } cfg.QPS = opts.QPS cfg.Burst = opts.Burst scheme := NewScheme() kubeClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: scheme, }) if err != nil { return nil, fmt.Errorf("kubernetes client initialization failed: %w", err) } return kubeClient, nil } // SplitKubeConfigPath splits the given KUBECONFIG path based on the runtime OS // target. // // Ref: https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable func SplitKubeConfigPath(path string) []string { var sep string switch runtime.GOOS { case "windows": sep = ";" default: sep = ":" } return strings.Split(path, sep) } func ContainsItemString(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } func ContainsEqualFoldItemString(s []string, e string) (string, bool) { for _, a := range s { if strings.EqualFold(a, e) { return a, true } } return "", false } // ParseNamespacedName extracts the NamespacedName of a resource // based on the '/' format func ParseNamespacedName(input string) types.NamespacedName { parts := strings.Split(input, "/") if len(parts) == 2 { return types.NamespacedName{ Namespace: parts[0], Name: parts[1], } } return types.NamespacedName{ Name: input, } } // ParseObjectKindName extracts the kind and name of a resource // based on the '/' format func ParseObjectKindName(input string) (kind, name string) { name = input parts := strings.Split(input, "/") if len(parts) == 2 { kind, name = parts[0], parts[1] } return kind, name } // ParseObjectKindNameNamespace extracts the kind, name and namespace of a resource // based on the '/.' format func ParseObjectKindNameNamespace(input string) (kind, name, namespace string) { kind, name = ParseObjectKindName(input) if nn := strings.Split(name, "."); len(nn) > 1 { name = strings.Join(nn[:len(nn)-1], ".") namespace = nn[len(nn)-1] } return kind, name, namespace } func MakeDependsOn(deps []string) []meta.NamespacedObjectReference { refs := []meta.NamespacedObjectReference{} for _, dep := range deps { parts := strings.Split(dep, "/") depNamespace := "" depName := "" if len(parts) > 1 { depNamespace = parts[0] depName = parts[1] } else { depName = parts[0] } refs = append(refs, meta.NamespacedObjectReference{ Namespace: depNamespace, Name: depName, }) } return refs } func ValidateComponents(components []string) error { defaults := install.MakeDefaultOptions() bootstrapAllComponents := append(defaults.Components, defaults.ComponentsExtra...) for _, component := range components { if !ContainsItemString(bootstrapAllComponents, component) { return fmt.Errorf("component %s is not available", component) } } return nil } // CompatibleVersion returns if the provided binary version is compatible // with the given target version. At present, this is true if the target // version is equal to the MINOR range of the binary, or if the binary // version is a prerelease. func CompatibleVersion(binary, target string) bool { binSv, err := version.ParseVersion(binary) if err != nil { return false } // Assume prerelease builds are compatible. if binSv.Prerelease() != "" { return true } targetSv, err := version.ParseVersion(target) if err != nil { return false } return binSv.Major() == targetSv.Major() && binSv.Minor() == targetSv.Minor() } func ExtractCRDs(inManifestPath, outManifestPath string) error { manifests, err := os.ReadFile(inManifestPath) if err != nil { return err } crds := "" reader := sigyaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifests), 2048) for { var obj unstructured.Unstructured err := reader.Decode(&obj) if err == io.EOF { break } else if err != nil { return err } if obj.GetKind() == "CustomResourceDefinition" { b, err := obj.MarshalJSON() if err != nil { return err } y, err := yaml.JSONToYAML(b) if err != nil { return err } crds += "---\n" + string(y) } } if crds == "" { return fmt.Errorf("no CRDs found in %s", inManifestPath) } return os.WriteFile(outManifestPath, []byte(crds), os.ModePerm) } ================================================ FILE: internal/utils/utils_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package utils import ( "path/filepath" "reflect" "testing" "github.com/fluxcd/pkg/apis/meta" ) func TestCompatibleVersion(t *testing.T) { tests := []struct { name string binary string target string want bool }{ {"different major version", "1.1.0", "0.1.0", false}, {"different minor version", "0.1.0", "0.2.0", false}, {"same version", "0.1.0", "0.1.0", true}, {"binary patch version ahead", "0.1.1", "0.1.0", true}, {"target patch version ahead", "0.1.1", "0.1.2", true}, {"prerelease binary", "0.0.0-dev.0", "0.1.0", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := CompatibleVersion(tt.binary, tt.target); got != tt.want { t.Errorf("CompatibleVersion() = %v, want %v", got, tt.want) } }) } } func TestParseObjectKindNameNamespace(t *testing.T) { tests := []struct { name string input string wantKind string wantName string wantNamespace string }{ {"with kind name namespace", "Kustomization/foo.flux-system", "Kustomization", "foo", "flux-system"}, {"without namespace", "Kustomization/foo", "Kustomization", "foo", ""}, {"name with dots", "Kustomization/foo.bar.flux-system", "Kustomization", "foo.bar", "flux-system"}, {"multiple slashes", "foo/bar/baz", "", "foo/bar/baz", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotKind, gotName, gotNamespace := ParseObjectKindNameNamespace(tt.input) if gotKind != tt.wantKind { t.Errorf("kind = %s, want %s", gotKind, tt.wantKind) } if gotName != tt.wantName { t.Errorf("name = %s, want %s", gotName, tt.wantName) } if gotNamespace != tt.wantNamespace { t.Errorf("namespace = %s, want %s", gotNamespace, tt.wantNamespace) } }) } } func TestMakeDependsOn(t *testing.T) { input := []string{ "someNSA/someNameA", "someNSB/someNameB", "someNameC", "someNSD/", "", } want := []meta.NamespacedObjectReference{ {Namespace: "someNSA", Name: "someNameA"}, {Namespace: "someNSB", Name: "someNameB"}, {Namespace: "", Name: "someNameC"}, {Namespace: "someNSD", Name: ""}, {Namespace: "", Name: ""}, } got := MakeDependsOn(input) if !reflect.DeepEqual(got, want) { t.Errorf("MakeDependsOn() = %v, want %v", got, want) } } func TestValidateComponents(t *testing.T) { tests := []struct { name string input []string expectErr bool }{ {"default and extra components", []string{"source-controller", "image-reflector-controller"}, false}, {"unknown components", []string{"some-comp-1", "some-comp-2"}, true}, {"mix of default and unknown", []string{"source-controller", "some-comp-1"}, true}, {"empty", []string{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := ValidateComponents(tt.input); (err != nil) != tt.expectErr { t.Errorf("ValidateComponents() error = %v, expectErr %v", err, tt.expectErr) } }) } } func TestExtractCRDs(t *testing.T) { tests := []struct { name string inManifestFile string expectErr bool }{ {"with crds", "components-with-crds.yaml", false}, {"without crds", "components-without-crds.yaml", true}, {"non-existent file", "non-existent-file.yaml", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { outManifestPath := filepath.Join(t.TempDir(), "crds.yaml") inManifestPath := filepath.Join("testdata", tt.inManifestFile) if err := ExtractCRDs(inManifestPath, outManifestPath); (err != nil) != tt.expectErr { t.Errorf("ExtractCRDs() error = %v, expectErr %v", err, tt.expectErr) } }) } } ================================================ FILE: manifests/bases/helm-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: helm-controller ================================================ FILE: manifests/bases/helm-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.crds.yaml - https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: helm-controller path: patch.yaml ================================================ FILE: manifests/bases/helm-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: helm-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/helm-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: helm-controller - op: add path: /spec/template/spec/priorityClassName value: system-cluster-critical - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/image-automation-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: image-automation-controller ================================================ FILE: manifests/bases/image-automation-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.crds.yaml - https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: image-automation-controller path: patch.yaml ================================================ FILE: manifests/bases/image-automation-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: image-automation-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/image-automation-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: image-automation-controller - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/image-reflector-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: image-reflector-controller ================================================ FILE: manifests/bases/image-reflector-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml - https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: image-reflector-controller path: patch.yaml ================================================ FILE: manifests/bases/image-reflector-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: image-reflector-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/image-reflector-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: image-reflector-controller - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/kustomize-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: kustomize-controller ================================================ FILE: manifests/bases/kustomize-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml - https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: kustomize-controller path: patch.yaml ================================================ FILE: manifests/bases/kustomize-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: kustomize-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/kustomize-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: kustomize-controller - op: add path: /spec/template/spec/priorityClassName value: system-cluster-critical - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/notification-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: notification-controller ================================================ FILE: manifests/bases/notification-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml - https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: notification-controller path: patch.yaml ================================================ FILE: manifests/bases/notification-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: notification-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/notification-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/serviceAccountName value: notification-controller - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/source-controller/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: source-controller ================================================ FILE: manifests/bases/source-controller/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml - https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: source-controller path: patch.yaml ================================================ FILE: manifests/bases/source-controller/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: source-controller app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/source-controller/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: source-controller - op: add path: /spec/template/spec/priorityClassName value: system-cluster-critical - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/bases/source-watcher/account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: source-watcher ================================================ FILE: manifests/bases/source-watcher/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml - https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.deployment.yaml - account.yaml transformers: - labels.yaml patches: - target: group: apps version: v1 kind: Deployment name: source-watcher path: patch.yaml ================================================ FILE: manifests/bases/source-watcher/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/component: source-watcher app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/bases/source-watcher/patch.yaml ================================================ - op: add path: /spec/template/spec/containers/0/args/0 value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: source-watcher - op: add path: /spec/template/spec/priorityClassName value: system-cluster-critical - op: add path: /spec/template/spec/containers/0/env/- value: name: GOMEMLIMIT valueFrom: resourceFieldRef: containerName: manager resource: limits.memory ================================================ FILE: manifests/crds/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml - https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml - https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.crds.yaml - https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml - https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml - https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.crds.yaml - https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml ================================================ FILE: manifests/install/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: flux-system resources: - namespace.yaml - ../bases/source-controller - ../bases/kustomize-controller - ../bases/notification-controller - ../bases/helm-controller - ../bases/image-reflector-controller - ../bases/image-automation-controller - ../rbac - ../policies transformers: - labels.yaml images: - name: fluxcd/source-controller newName: ghcr.io/fluxcd/source-controller - name: fluxcd/kustomize-controller newName: ghcr.io/fluxcd/kustomize-controller - name: fluxcd/helm-controller newName: ghcr.io/fluxcd/helm-controller - name: fluxcd/notification-controller newName: ghcr.io/fluxcd/notification-controller - name: fluxcd/image-reflector-controller newName: ghcr.io/fluxcd/image-reflector-controller - name: fluxcd/image-automation-controller newName: ghcr.io/fluxcd/image-automation-controller ================================================ FILE: manifests/install/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/part-of: flux app.kubernetes.io/instance: flux-system fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/install/namespace.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: flux-system labels: pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: latest ================================================ FILE: manifests/monitoring/README.md ================================================ # :warning: Removal Notice Starting Flux v2.1.0, released August 24, 2023, the Flux monitoring configurations in this repository were marked as deprecated. The new monitoring docs are available at [Flux monitoring](https://fluxcd.io/flux/monitoring/) docs with new example configurations in [fluxcd/flux2-monitoring-example](https://github.com/fluxcd/flux2-monitoring-example/). The deprecated configurations were removed in Flux v2.2 on December 13, 2023. All users of these configurations are advised to use the new monitoring setup, following the [docs](https://fluxcd.io/flux/monitoring/) and the [examples](https://github.com/fluxcd/flux2-monitoring-example/). After collecting a lot of user feedback about our monitoring recommendation, in order to serve most of the needs of the users, we decided to create a new monitoring setup leveraging more of the kube-prometheus-stack, specifically kube-state-metrics, to enable configuring Flux custom metrics, see the [Flux custom Prometheus metrics](https://fluxcd.io/flux/monitoring/custom-metrics/) docs to learn more about it. Please refer to [fluxcd/flux2/4128](https://github.com/fluxcd/flux2/issues/4128) for a detailed explanation about this change and the new capabilities offered by the new monitoring setup. ================================================ FILE: manifests/openshift/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: flux-system resources: - namespace.yaml - scc.yaml - ../bases/source-controller - ../bases/source-watcher - ../bases/kustomize-controller - ../bases/notification-controller - ../bases/helm-controller - ../bases/image-reflector-controller - ../bases/image-automation-controller - ../rbac - ../policies transformers: - labels.yaml images: - name: fluxcd/source-controller newName: ghcr.io/fluxcd/source-controller - name: fluxcd/source-watcher newName: ghcr.io/fluxcd/source-watcher - name: fluxcd/kustomize-controller newName: ghcr.io/fluxcd/kustomize-controller - name: fluxcd/helm-controller newName: ghcr.io/fluxcd/helm-controller - name: fluxcd/notification-controller newName: ghcr.io/fluxcd/notification-controller - name: fluxcd/image-reflector-controller newName: ghcr.io/fluxcd/image-reflector-controller - name: fluxcd/image-automation-controller newName: ghcr.io/fluxcd/image-automation-controller patches: - patch: | apiVersion: apps/v1 kind: Deployment metadata: name: all spec: template: spec: securityContext: $patch: delete containers: - name: manager securityContext: runAsUser: 65534 seccompProfile: $patch: delete target: kind: Deployment ================================================ FILE: manifests/openshift/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/part-of: flux app.kubernetes.io/instance: flux-system fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/openshift/namespace.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: flux-system ================================================ FILE: manifests/openshift/scc.yaml ================================================ # Allow Flux controllers to run as non-root on OpenShift # Docs: https://fluxcd.io/flux/installation/configuration/openshift/ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: flux-scc rules: - apiGroups: - security.openshift.io resources: - securitycontextconstraints resourceNames: - nonroot verbs: - use --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: flux-scc roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: flux-scc subjects: - kind: ServiceAccount name: source-controller namespace: flux-system - kind: ServiceAccount name: source-watcher namespace: flux-system - kind: ServiceAccount name: kustomize-controller namespace: flux-system - kind: ServiceAccount name: helm-controller namespace: flux-system - kind: ServiceAccount name: notification-controller namespace: flux-system - kind: ServiceAccount name: image-reflector-controller namespace: flux-system - kind: ServiceAccount name: image-automation-controller namespace: flux-system ================================================ FILE: manifests/policies/allow-egress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-egress spec: policyTypes: - Ingress - Egress ingress: - from: - podSelector: {} egress: - {} podSelector: {} ================================================ FILE: manifests/policies/allow-scraping.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-scraping spec: policyTypes: - Ingress ingress: - from: - namespaceSelector: {} ports: - protocol: TCP port: 8080 podSelector: {} ================================================ FILE: manifests/policies/allow-webhooks.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-webhooks spec: policyTypes: - Ingress ingress: - from: - namespaceSelector: {} podSelector: matchLabels: app: notification-controller ================================================ FILE: manifests/policies/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - allow-egress.yaml - allow-scraping.yaml - allow-webhooks.yaml ================================================ FILE: manifests/rbac/controller.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: crd-controller rules: - apiGroups: ['source.toolkit.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: ['kustomize.toolkit.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: ['helm.toolkit.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: ['notification.toolkit.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: ['image.toolkit.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: ['source.extensions.fluxcd.io'] resources: ['*'] verbs: ['*'] - apiGroups: - "" resources: - namespaces - secrets - configmaps - serviceaccounts verbs: - get - list - watch - apiGroups: - "" resources: - events verbs: - create - patch # required by leader election - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - configmaps/status verbs: - get - update - patch - apiGroups: - "coordination.k8s.io" resources: - leases verbs: - get - list - watch - create - update - patch - delete # required for object-level workload identity - apiGroups: - "" resources: - serviceaccounts/token verbs: - create # required for flow control - nonResourceURLs: - /livez/ping verbs: - head --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: crd-controller roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: crd-controller subjects: - kind: ServiceAccount name: kustomize-controller namespace: flux-system - kind: ServiceAccount name: helm-controller namespace: flux-system - kind: ServiceAccount name: source-controller namespace: flux-system - kind: ServiceAccount name: notification-controller namespace: flux-system - kind: ServiceAccount name: image-reflector-controller namespace: flux-system - kind: ServiceAccount name: image-automation-controller namespace: flux-system - kind: ServiceAccount name: source-watcher namespace: flux-system ================================================ FILE: manifests/rbac/edit.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: flux-edit labels: rbac.authorization.k8s.io/aggregate-to-edit: "true" rbac.authorization.k8s.io/aggregate-to-admin: "true" rules: - apiGroups: - notification.toolkit.fluxcd.io - source.toolkit.fluxcd.io - source.extensions.fluxcd.io - helm.toolkit.fluxcd.io - image.toolkit.fluxcd.io - kustomize.toolkit.fluxcd.io resources: ["*"] verbs: - create - delete - deletecollection - patch - update ================================================ FILE: manifests/rbac/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - controller.yaml - reconciler.yaml - edit.yaml - view.yaml - resourcequota.yaml ================================================ FILE: manifests/rbac/reconciler.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cluster-reconciler roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: kustomize-controller namespace: flux-system - kind: ServiceAccount name: helm-controller namespace: flux-system ================================================ FILE: manifests/rbac/resourcequota.yaml ================================================ apiVersion: v1 kind: ResourceQuota metadata: name: critical-pods spec: hard: pods: "1000" scopeSelector: matchExpressions: - operator: In scopeName: PriorityClass values: - system-node-critical - system-cluster-critical ================================================ FILE: manifests/rbac/view.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: flux-view labels: rbac.authorization.k8s.io/aggregate-to-admin: "true" rbac.authorization.k8s.io/aggregate-to-edit: "true" rbac.authorization.k8s.io/aggregate-to-view: "true" rules: - apiGroups: - notification.toolkit.fluxcd.io - source.toolkit.fluxcd.io - source.extensions.fluxcd.io - helm.toolkit.fluxcd.io - image.toolkit.fluxcd.io - kustomize.toolkit.fluxcd.io resources: ["*"] verbs: - get - list - watch ================================================ FILE: manifests/scripts/bundle.sh ================================================ #!/usr/bin/env bash # Copyright 2020, 2021 The Flux authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -e IN_PATH=${1:-"$(realpath $(dirname "${BASH_SOURCE[0]}")/../..)/manifests"} OUT_PATH=${2:-"$(realpath $(dirname "${BASH_SOURCE[0]}")/../..)/cmd/flux/manifests"} TAR=${3} info() { echo '[INFO] ' "$@" } fatal() { echo '[ERROR] ' "$@" >&2 exit 1 } build() { info "building $(basename $2)" kustomize build "$1" > "$2" } if ! [ -x "$(command -v kustomize)" ]; then fatal 'kustomize is not installed' fi rm -rf $OUT_PATH mkdir -p $OUT_PATH files="" info using kustomize "$(kustomize version)" # build controllers for controller in ${IN_PATH}/bases/*/; do output_path="${OUT_PATH}/$(basename $controller).yaml" build $controller $output_path files+=" $(basename $output_path)" done # build rbac rbac_path="${IN_PATH}/rbac" rbac_output_path="${OUT_PATH}/rbac.yaml" build $rbac_path $rbac_output_path files+=" $(basename $rbac_output_path)" # build policies policies_path="${IN_PATH}/policies" policies_output_path="${OUT_PATH}/policies.yaml" build $policies_path $policies_output_path files+=" $(basename $policies_output_path)" # create tarball if [[ -n $TAR ]];then info "archiving $TAR" cd ${OUT_PATH} && tar -czf $TAR $files fi ================================================ FILE: manifests/test/kustomization.yaml ================================================ # This overlay is used in end-to-end tests and contains all optional controllers. apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: flux-system resources: - namespace.yaml - ../bases/source-controller - ../bases/source-watcher - ../bases/kustomize-controller - ../bases/notification-controller - ../bases/helm-controller - ../bases/image-reflector-controller - ../bases/image-automation-controller - ../rbac - ../policies transformers: - labels.yaml images: - name: fluxcd/source-controller newName: ghcr.io/fluxcd/source-controller - name: fluxcd/source-watcher newName: ghcr.io/fluxcd/source-watcher - name: fluxcd/kustomize-controller newName: ghcr.io/fluxcd/kustomize-controller - name: fluxcd/helm-controller newName: ghcr.io/fluxcd/helm-controller - name: fluxcd/notification-controller newName: ghcr.io/fluxcd/notification-controller - name: fluxcd/image-reflector-controller newName: ghcr.io/fluxcd/image-reflector-controller - name: fluxcd/image-automation-controller newName: ghcr.io/fluxcd/image-automation-controller patches: - target: kind: Deployment name: "(kustomize-controller|helm-controller)" patch: |- - op: add path: /spec/template/spec/containers/0/args/- value: --feature-gates=ExternalArtifact=true ================================================ FILE: manifests/test/labels.yaml ================================================ apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/part-of: flux app.kubernetes.io/instance: flux-system fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ================================================ FILE: manifests/test/namespace.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: flux-system labels: pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: latest ================================================ FILE: netlify.toml ================================================ [build] command = "mkdir -p site && cp docs/_redirects site/" publish = "site" ================================================ FILE: pkg/bootstrap/bootstrap.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bootstrap import ( "context" "errors" "fmt" "strings" "time" corev1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" apierrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" ) var ( ErrReconciledWithWarning = errors.New("reconciled with warning") ) // Reconciler reconciles and reports the health of different // components and kubernetes resources involved in the installation of Flux. // // It is recommended use the `ReconcilerWithSyncCheck` interface that also // reports the health of the GitRepository. type Reconciler interface { // ReconcileComponents reconciles the components by generating the // manifests with the provided values, committing them to Git and // pushing to remote if there are any changes, and applying them // to the cluster. ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, secretOpts sourcesecret.Options) error // ReconcileSourceSecret reconciles the source secret by generating // a new secret with the provided values if the secret does not // already exists on the cluster, or if any of the configuration // options changed. ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error // ReconcileSyncConfig reconciles the sync configuration by generating // the sync manifests with the provided values, committing them to Git // and pushing to remote if there are any changes. ReconcileSyncConfig(ctx context.Context, options sync.Options) error // ReportKustomizationHealth reports about the health of the // Kustomization synchronizing the components. ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error // ReportComponentsHealth reports about the health for the components // and extra components in install.Options. ReportComponentsHealth(ctx context.Context, options install.Options, timeout time.Duration) error } type RepositoryReconciler interface { // ReconcileRepository reconciles an external Git repository. ReconcileRepository(ctx context.Context) error } // ReconcilerWithSyncCheck extends the Reconciler interface to also report the health of the GitReposiotry // that syncs Flux on the cluster type ReconcilerWithSyncCheck interface { Reconciler // ReportGitRepoHealth reports about the health of the GitRepository synchronizing the components. ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error } type PostGenerateSecretFunc func(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error func Run(ctx context.Context, reconciler Reconciler, manifestsBase string, installOpts install.Options, secretOpts sourcesecret.Options, syncOpts sync.Options, pollInterval, timeout time.Duration) error { var err error if r, ok := reconciler.(RepositoryReconciler); ok { if err = r.ReconcileRepository(ctx); err != nil && !errors.Is(err, ErrReconciledWithWarning) { return err } } if err := reconciler.ReconcileComponents(ctx, manifestsBase, installOpts, secretOpts); err != nil { return err } if err := reconciler.ReconcileSourceSecret(ctx, secretOpts); err != nil { return err } if err := reconciler.ReconcileSyncConfig(ctx, syncOpts); err != nil { return err } var errs []error if r, ok := reconciler.(ReconcilerWithSyncCheck); ok { if err := r.ReportGitRepoHealth(ctx, syncOpts, pollInterval, timeout); err != nil { errs = append(errs, err) } } if err := reconciler.ReportKustomizationHealth(ctx, syncOpts, pollInterval, timeout); err != nil { errs = append(errs, err) } if err := reconciler.ReportComponentsHealth(ctx, installOpts, timeout); err != nil { errs = append(errs, err) } if len(errs) > 0 { err = fmt.Errorf("bootstrap failed with %d health check failure(s): %w", len(errs), apierrors.NewAggregate(errs)) } return err } func mustInstallManifests(ctx context.Context, kube client.Client, namespace string) bool { namespacedName := types.NamespacedName{ Namespace: namespace, Name: namespace, } var k kustomizev1.Kustomization if err := kube.Get(ctx, namespacedName, &k); err != nil { return true } return k.Status.LastAppliedRevision == "" } func secretExists(ctx context.Context, kube client.Client, objKey client.ObjectKey) (bool, error) { if err := kube.Get(ctx, objKey, &corev1.Secret{}); err != nil { if apierr.IsNotFound(err) { return false, nil } return false, err } return true, nil } func reconcileSecret(ctx context.Context, kube client.Client, secret corev1.Secret) error { objKey := client.ObjectKeyFromObject(&secret) var existing corev1.Secret err := kube.Get(ctx, objKey, &existing) if err != nil { if apierr.IsNotFound(err) { return kube.Create(ctx, &secret) } return err } existing.StringData = secret.StringData return kube.Update(ctx, &existing) } func reconcileImagePullSecret(ctx context.Context, kube client.Client, installOpts install.Options) error { credentials := strings.SplitN(installOpts.RegistryCredential, ":", 2) dcj, err := sourcesecret.GenerateDockerConfigJson(installOpts.Registry, credentials[0], credentials[1]) if err != nil { return fmt.Errorf("failed to generate docker config json: %w", err) } secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: installOpts.Namespace, Name: installOpts.ImagePullSecret, }, StringData: map[string]string{ corev1.DockerConfigJsonKey: string(dcj), }, Type: corev1.SecretTypeDockerConfigJson, } return reconcileSecret(ctx, kube, secret) } func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey client.ObjectKey, path string) (string, error) { var k kustomizev1.Kustomization if err := kube.Get(ctx, objKey, &k); err != nil { if apierr.IsNotFound(err) { return "", nil } return "", err } normalizePath := func(p string) string { // remove the trailing '/' if the path is not './' if len(p) > 2 { p = strings.TrimSuffix(p, "/") } return fmt.Sprintf("./%s", strings.TrimPrefix(p, "./")) } if normalizePath(path) == normalizePath(k.Spec.Path) { return "", nil } return k.Spec.Path, nil } type objectWithConditions interface { client.Object GetConditions() []metav1.Condition } func objectReconciled(kube client.Client, objKey client.ObjectKey, clientObject objectWithConditions, expectRevision string) wait.ConditionWithContextFunc { return func(ctx context.Context) (bool, error) { // for some reason, TypeMeta gets unset after kube.Get so we want to store the GVK and set it after // ref https://github.com/kubernetes-sigs/controller-runtime/issues/1517#issuecomment-844703142 gvk := clientObject.GetObjectKind().GroupVersionKind() if err := kube.Get(ctx, objKey, clientObject); err != nil { return false, err } clientObject.GetObjectKind().SetGroupVersionKind(gvk) kind := gvk.Kind obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(clientObject) if err != nil { return false, err } // Detect suspended object, as this would result in an endless wait if suspended, ok, _ := unstructured.NestedBool(obj, "spec", "suspend"); ok && suspended { return false, fmt.Errorf("%s '%s' is suspended", kind, objKey.String()) } // Confirm the state we are observing is for the current generation if generation, ok, _ := unstructured.NestedInt64(obj, "status", "observedGeneration"); ok && generation != clientObject.GetGeneration() { return false, nil } // Confirm the resource is healthy if c := apimeta.FindStatusCondition(clientObject.GetConditions(), meta.ReadyCondition); c != nil { switch c.Status { case metav1.ConditionTrue: // Confirm the given revision has been attempted by the controller hasRev, err := hasRevision(kind, obj, expectRevision) if err != nil { return false, err } return hasRev, nil case metav1.ConditionFalse: return false, errors.New(c.Message) } } return false, nil } } // hasRevision checks that the reconciled revision (for Kustomization this is `.status.lastAttemptedRevision` // and for Source APIs, it is stored in `.status.artifact.revision`) is the same as the expectedRev func hasRevision(kind string, obj map[string]interface{}, expectedRev string) (bool, error) { var rev string switch kind { case sourcev1.GitRepositoryKind, sourcev1.OCIRepositoryKind, sourcev1.BucketKind, sourcev1.HelmChartKind: rev, _, _ = unstructured.NestedString(obj, "status", "artifact", "revision") case kustomizev1.KustomizationKind: rev, _, _ = unstructured.NestedString(obj, "status", "lastAttemptedRevision") default: return false, fmt.Errorf("cannot get status revision for kind: '%s'", kind) } return sourcev1b2.TransformLegacyRevision(rev) == expectedRev, nil } func retry(retries int, wait time.Duration, fn func() error) (err error) { for i := 0; ; i++ { err = fn() if err == nil { return } if i >= retries { break } time.Sleep(wait) } return err } ================================================ FILE: pkg/bootstrap/bootstrap_plain_git.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bootstrap import ( "context" "errors" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/ProtonMail/go-crypto/openpgp" gogit "github.com/go-git/go-git/v5" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/yaml" "github.com/fluxcd/cli-utils/pkg/object" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/kustomize/filesys" runclient "github.com/fluxcd/pkg/runtime/client" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/log" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/v2/pkg/status" ) type PlainGitBootstrapper struct { url string branch string signature git.Signature commitMessageAppendix string gpgKeyRing openpgp.EntityList gpgPassphrase string gpgKeyID string restClientGetter genericclioptions.RESTClientGetter restClientOptions *runclient.Options postGenerateSecret []PostGenerateSecretFunc gitClient repository.Client kube client.Client logger log.Logger } type GitOption interface { applyGit(b *PlainGitBootstrapper) } func WithRepositoryURL(url string) GitOption { return repositoryURLOption(url) } type repositoryURLOption string func (o repositoryURLOption) applyGit(b *PlainGitBootstrapper) { b.url = string(o) } func WithPostGenerateSecretFunc(callback PostGenerateSecretFunc) GitOption { return postGenerateSecret(callback) } type postGenerateSecret PostGenerateSecretFunc func (o postGenerateSecret) applyGit(b *PlainGitBootstrapper) { b.postGenerateSecret = append(b.postGenerateSecret, PostGenerateSecretFunc(o)) } func NewPlainGitProvider(git repository.Client, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) { b := &PlainGitBootstrapper{ gitClient: git, kube: kube, } for _, opt := range opts { opt.applyGit(b) } return b, nil } func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, _ sourcesecret.Options) error { // Clone if not already if _, err := b.gitClient.Head(); err != nil { if err != git.ErrNoGitRepository { return err } b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) var cloned bool if err = retry(1, 2*time.Second, func() (err error) { if err = b.cleanGitRepoDir(); err != nil { b.logger.Warningf(" failed to clean directory for git repo: %w", err) return } _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ CheckoutStrategy: repository.CheckoutStrategy{ Branch: b.branch, }, }) if err != nil { b.logger.Warningf(" clone failure: %s", err) } if err == nil { cloned = true } return }); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } if cloned { b.logger.Successf("cloned repository") } } // Generate component manifests b.logger.Actionf("generating component manifests") manifests, err := install.Generate(options, manifestsBase) if err != nil { return fmt.Errorf("component manifest generation failed: %w", err) } b.logger.Successf("generated component manifests") // Write generated files and make a commit var signer *openpgp.Entity if b.gpgKeyRing != nil { signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) if err != nil { return fmt.Errorf("failed to generate OpenPGP entity: %w", err) } } commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix } commit, err := b.gitClient.Commit(git.Commit{ Author: b.signature, Message: commitMsg, }, repository.WithFiles(map[string]io.Reader{ manifests.Path: strings.NewReader(manifests.Content), }), repository.WithSigner(signer)) if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit component manifests: %w", err) } if err == nil { b.logger.Successf("committed component manifests to %q (%q)", b.branch, commit) b.logger.Actionf("pushing component manifests to %q", b.url) if err = b.gitClient.Push(ctx, repository.PushConfig{}); err != nil { return fmt.Errorf("failed to push manifests: %w", err) } } else { b.logger.Successf("component manifests are up to date") } // Conditionally install manifests if mustInstallManifests(ctx, b.kube, options.Namespace) { b.logger.Actionf("installing components in %q namespace", options.Namespace) componentsYAML := filepath.Join(b.gitClient.Path(), manifests.Path) kfile := filepath.Join(filepath.Dir(componentsYAML), konfig.DefaultKustomizationFileName()) if _, err := os.Stat(kfile); err == nil { // Apply the components and their patches if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), kfile); err != nil { return err } } else { // Apply the CRDs and controllers if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), componentsYAML); err != nil { return err } } b.logger.Successf("installed components") } // Reconcile image pull secret if needed if options.ImagePullSecret != "" && options.RegistryCredential != "" { if err := reconcileImagePullSecret(ctx, b.kube, options); err != nil { return fmt.Errorf("failed to reconcile image pull secret: %w", err) } b.logger.Successf("reconciled image pull secret %s", options.ImagePullSecret) } b.logger.Successf("reconciled components") return nil } func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { // Determine if there is an existing secret secretKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} b.logger.Actionf("determining if source secret %q exists", secretKey) ok, err := secretExists(ctx, b.kube, secretKey) if err != nil { return fmt.Errorf("failed to determine if deploy key secret exists: %w", err) } // Return early if exists and no custom config is passed if ok && options.Keypair == nil && len(options.CACrt) == 0 && len(options.Username+options.Password) == 0 { b.logger.Successf("source secret up to date") return nil } // Generate source secret b.logger.Actionf("generating source secret") manifest, err := sourcesecret.GenerateGit(options) if err != nil { return err } var secret corev1.Secret if err := yaml.Unmarshal([]byte(manifest.Content), &secret); err != nil { return fmt.Errorf("failed to unmarshal generated source secret manifest: %w", err) } for _, callback := range b.postGenerateSecret { if err = callback(ctx, secret, options); err != nil { return err } } // Apply source secret b.logger.Actionf("applying source secret %q", secretKey) if err = reconcileSecret(ctx, b.kube, secret); err != nil { return err } b.logger.Successf("reconciled source secret") return nil } func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { // Confirm that sync configuration does not overwrite existing config if curPath, err := kustomizationPathDiffers(ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, options.TargetPath); err != nil { return fmt.Errorf("failed to determine if sync configuration would overwrite existing Kustomization: %w", err) } else if curPath != "" { return fmt.Errorf("sync path configuration (%q) would overwrite path (%q) of existing Kustomization", options.TargetPath, curPath) } // Clone if not already if _, err := b.gitClient.Head(); err != nil { if err == git.ErrNoGitRepository { b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) var cloned bool if err = retry(1, 2*time.Second, func() (err error) { if err = b.cleanGitRepoDir(); err != nil { b.logger.Warningf(" failed to clean directory for git repo: %w", err) return } _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ CheckoutStrategy: repository.CheckoutStrategy{ Branch: b.branch, }, }) if err != nil { b.logger.Warningf(" clone failure: %s", err) } if err == nil { cloned = true } return }); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } if cloned { b.logger.Successf("cloned repository") } } return err } // Generate sync manifests and write to Git repository b.logger.Actionf("generating sync manifests") manifests, err := sync.Generate(options) if err != nil { return fmt.Errorf("sync manifests generation failed: %w", err) } // Create secure Kustomize FS fs, err := filesys.MakeFsOnDiskSecureBuild(b.gitClient.Path()) if err != nil { return fmt.Errorf("failed to initialize Kustomize file system: %w", err) } if err = fs.WriteFile(filepath.Join(b.gitClient.Path(), manifests.Path), []byte(manifests.Content)); err != nil { return err } // Generate Kustomization kusManifests, err := kustomization.Generate(kustomization.Options{ FileSystem: fs, BaseDir: b.gitClient.Path(), TargetPath: filepath.Dir(manifests.Path), }) if err != nil { return fmt.Errorf("%s generation failed: %w", konfig.DefaultKustomizationFileName(), err) } b.logger.Successf("generated sync manifests") // Write generated files and make a commit var signer *openpgp.Entity if b.gpgKeyRing != nil { signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) if err != nil { return fmt.Errorf("failed to generate OpenPGP entity: %w", err) } } commitMsg := "Add Flux sync manifests" if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix } commit, err := b.gitClient.Commit(git.Commit{ Author: b.signature, Message: commitMsg, }, repository.WithFiles(map[string]io.Reader{ kusManifests.Path: strings.NewReader(kusManifests.Content), }), repository.WithSigner(signer)) if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit sync manifests: %w", err) } if err == nil { b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit) b.logger.Actionf("pushing sync manifests to %q", b.url) err = b.gitClient.Push(ctx, repository.PushConfig{}) if err != nil { if strings.HasPrefix(err.Error(), gogit.ErrNonFastForwardUpdate.Error()) { b.logger.Waitingf("git conflict detected, retrying with a fresh clone") if err := os.RemoveAll(b.gitClient.Path()); err != nil { return fmt.Errorf("failed to remove tmp dir: %w", err) } if err := os.Mkdir(b.gitClient.Path(), 0o700); err != nil { return fmt.Errorf("failed to recreate tmp dir: %w", err) } if err = retry(1, 2*time.Second, func() (err error) { if err = b.cleanGitRepoDir(); err != nil { b.logger.Warningf(" failed to clean directory for git repo: %w", err) return } _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ CheckoutStrategy: repository.CheckoutStrategy{ Branch: b.branch, }, }) if err != nil { b.logger.Warningf(" clone failure: %s", err) } return }); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } return b.ReconcileSyncConfig(ctx, options) } return fmt.Errorf("failed to push sync manifests: %w", err) } } else { b.logger.Successf("sync manifests are up to date") } // Apply to cluster b.logger.Actionf("applying sync manifests") if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), filepath.Join(b.gitClient.Path(), kusManifests.Path)); err != nil { return err } b.logger.Successf("reconciled sync configuration") return nil } func (b *PlainGitBootstrapper) ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error { head, err := b.gitClient.Head() if err != nil { return err } objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest()) b.logger.Waitingf("waiting for Kustomization %q to be reconciled", objKey.String()) k := &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, }, } if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, objectReconciled(b.kube, objKey, k, expectRevision)); err != nil { // If the poll timed out, we want to log the ready condition message as // that likely contains the reason if errors.Is(err, context.DeadlineExceeded) { readyCondition := apimeta.FindStatusCondition(k.Status.Conditions, meta.ReadyCondition) if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue { err = fmt.Errorf("kustomization '%s' not ready: '%s'", objKey, readyCondition.Message) } } b.logger.Failuref(err.Error()) return fmt.Errorf("error while waiting for Kustomization to be ready: '%s'", err) } b.logger.Successf("Kustomization reconciled successfully") return nil } func (b *PlainGitBootstrapper) ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error { head, err := b.gitClient.Head() if err != nil { return err } objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} b.logger.Waitingf("waiting for GitRepository %q to be reconciled", objKey.String()) expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest()) g := &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, } if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, objectReconciled(b.kube, objKey, g, expectRevision)); err != nil { // If the poll timed out, we want to log the ready condition message as // that likely contains the reason if errors.Is(err, context.DeadlineExceeded) { readyCondition := apimeta.FindStatusCondition(g.Status.Conditions, meta.ReadyCondition) if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue { err = fmt.Errorf("gitrepository '%s' not ready: '%s'", objKey, readyCondition.Message) } } b.logger.Failuref(err.Error()) return fmt.Errorf("error while waiting for GitRepository to be ready: '%s'", err) } b.logger.Successf("GitRepository reconciled successfully") return nil } func (b *PlainGitBootstrapper) ReportComponentsHealth(ctx context.Context, install install.Options, timeout time.Duration) error { cfg, err := utils.KubeConfig(b.restClientGetter, b.restClientOptions) if err != nil { return err } checker, err := status.NewStatusChecker(cfg, 5*time.Second, timeout, b.logger) if err != nil { return err } var components = install.Components components = append(components, install.ComponentsExtra...) var identifiers []object.ObjMetadata for _, component := range components { identifiers = append(identifiers, object.ObjMetadata{ Namespace: install.Namespace, Name: component, GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, }) } b.logger.Actionf("confirming components are healthy") if err := checker.Assess(identifiers...); err != nil { return err } b.logger.Successf("all components are healthy") return nil } // cleanGitRepoDir cleans the directory meant for the Git repo. func (b *PlainGitBootstrapper) cleanGitRepoDir() error { dirs, err := os.ReadDir(b.gitClient.Path()) if err != nil { return err } var errs []error for _, dir := range dirs { if err := os.RemoveAll(filepath.Join(b.gitClient.Path(), dir.Name())); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } func getOpenPgpEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) { if len(keyRing) == 0 { return nil, fmt.Errorf("empty GPG key ring") } var entity *openpgp.Entity if keyID != "" { keyID = strings.TrimPrefix(keyID, "0x") if len(keyID) != 16 { return nil, fmt.Errorf("invalid GPG key id length; expected %d, got %d", 16, len(keyID)) } keyID = strings.ToUpper(keyID) for _, ent := range keyRing { if ent.PrimaryKey.KeyIdString() == keyID { entity = ent } } if entity == nil { return nil, fmt.Errorf("no GPG keyring matching key id '%s' found", keyID) } if entity.PrivateKey == nil { return nil, fmt.Errorf("keyring does not contain private key for key id '%s'", keyID) } } else { entity = keyRing[0] } err := entity.PrivateKey.Decrypt([]byte(passphrase)) if err != nil { return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err) } return entity, nil } ================================================ FILE: pkg/bootstrap/bootstrap_provider.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bootstrap import ( "context" "errors" "fmt" "net/url" "strings" "time" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" "github.com/fluxcd/pkg/git/repository" ) type GitProviderBootstrapper struct { *PlainGitBootstrapper owner string repositoryName string repository gitprovider.UserRepository personal bool description string defaultBranch string visibility string reconcile bool teams map[string]string readWriteKey bool bootstrapTransportType string syncTransportType string sshHostname string useDeployTokenAuth bool provider gitprovider.Client } func NewGitProviderBootstrapper(git repository.Client, provider gitprovider.Client, kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) { b := &GitProviderBootstrapper{ PlainGitBootstrapper: &PlainGitBootstrapper{ gitClient: git, kube: kube, }, bootstrapTransportType: "https", syncTransportType: "ssh", provider: provider, } b.PlainGitBootstrapper.postGenerateSecret = append(b.PlainGitBootstrapper.postGenerateSecret, b.reconcileDeployKey) for _, opt := range opts { opt.applyGitProvider(b) } return b, nil } type GitProviderOption interface { applyGitProvider(b *GitProviderBootstrapper) } func WithProviderRepository(owner, repositoryName string, personal bool) GitProviderOption { return providerRepositoryOption{ owner: owner, repositoryName: repositoryName, personal: personal, } } func WithProviderVisibility(visibility string) GitProviderOption { return providerRepositoryConfigOption{ visibility: visibility, } } type providerRepositoryOption struct { owner string repositoryName string personal bool } func (o providerRepositoryOption) applyGitProvider(b *GitProviderBootstrapper) { b.owner = o.owner b.repositoryName = o.repositoryName b.personal = o.personal } func WithProviderRepositoryConfig(description, defaultBranch, visibility string) GitProviderOption { return providerRepositoryConfigOption{ description: description, defaultBranch: defaultBranch, visibility: visibility, } } type providerRepositoryConfigOption struct { description string defaultBranch string visibility string } func (o providerRepositoryConfigOption) applyGitProvider(b *GitProviderBootstrapper) { b.description = o.description b.defaultBranch = o.defaultBranch b.visibility = o.visibility } func WithProviderTeamPermissions(teams map[string]string) GitProviderOption { return providerRepositoryTeamPermissionsOption(teams) } type providerRepositoryTeamPermissionsOption map[string]string func (o providerRepositoryTeamPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) { b.teams = o } func WithReadWriteKeyPermissions(b bool) GitProviderOption { return withReadWriteKeyPermissionsOption(b) } type withReadWriteKeyPermissionsOption bool func (o withReadWriteKeyPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) { b.readWriteKey = bool(o) } func WithBootstrapTransportType(protocol string) GitProviderOption { return bootstrapTransportTypeOption(protocol) } type bootstrapTransportTypeOption string func (o bootstrapTransportTypeOption) applyGitProvider(b *GitProviderBootstrapper) { b.bootstrapTransportType = string(o) } func WithSyncTransportType(protocol string) GitProviderOption { return syncProtocolOption(protocol) } type syncProtocolOption string func (o syncProtocolOption) applyGitProvider(b *GitProviderBootstrapper) { b.syncTransportType = string(o) } func WithSSHHostname(hostname string) GitProviderOption { return sshHostnameOption(hostname) } type sshHostnameOption string func (o sshHostnameOption) applyGitProvider(b *GitProviderBootstrapper) { b.sshHostname = string(o) } func WithReconcile() GitProviderOption { return reconcileOption(true) } type reconcileOption bool func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) { b.reconcile = true } func WithDeployTokenAuth() GitProviderOption { return deployTokenAuthOption(true) } type deployTokenAuthOption bool func (o deployTokenAuthOption) applyGitProvider(b *GitProviderBootstrapper) { b.useDeployTokenAuth = true } func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { if b.repository == nil { return errors.New("repository is required") } if b.url == "" { bootstrapURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.bootstrapTransportType)) if err != nil { return err } WithRepositoryURL(bootstrapURL).applyGit(b.PlainGitBootstrapper) } if options.URL == "" { syncURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.syncTransportType)) if err != nil { return err } options.URL = syncURL } return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options) } func (b *GitProviderBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { if b.repository == nil { return errors.New("repository is required") } if b.useDeployTokenAuth { deployTokenInfo, err := b.reconcileDeployToken(ctx, options) if err != nil { return err } if deployTokenInfo != nil { options.Username = deployTokenInfo.Username options.Password = deployTokenInfo.Token } } return b.PlainGitBootstrapper.ReconcileSourceSecret(ctx, options) } // ReconcileRepository reconciles an organization or user repository with the // GitProviderBootstrapper configuration. On success, the URL in the embedded // PlainGitBootstrapper is set to clone URL for the configured protocol. // // When part of the reconciliation fails with a warning without aborting, an // ErrReconciledWithWarning error is returned. func (b *GitProviderBootstrapper) ReconcileRepository(ctx context.Context) error { var repo gitprovider.UserRepository var err error if b.personal { repo, err = b.reconcileUserRepository(ctx) } else { repo, err = b.reconcileOrgRepository(ctx) } if err != nil && !errors.Is(err, ErrReconciledWithWarning) { return err } cloneURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType)) if err != nil { return err } b.repository = repo WithRepositoryURL(cloneURL).applyGit(b.PlainGitBootstrapper) return err } func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error { if b.repository == nil { return errors.New("repository is required") } ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey] if !ok { return nil } b.logger.Successf("public key: %s", strings.TrimSpace(ppk)) name := deployKeyName(options.Namespace, b.branch, options.Name, options.TargetPath) deployKeyInfo := newDeployKeyInfo(name, ppk, b.readWriteKey) _, changed, err := b.repository.DeployKeys().Reconcile(ctx, deployKeyInfo) if err != nil { return err } if changed { b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, b.repository.Repository().String()) } return nil } func (b *GitProviderBootstrapper) reconcileDeployToken(ctx context.Context, options sourcesecret.Options) (*gitprovider.DeployTokenInfo, error) { dts, err := b.repository.DeployTokens() if err != nil { return nil, err } b.logger.Actionf("checking to reconcile deploy token for source secret") name := deployTokenName(options.Namespace, b.branch, options.Name, options.TargetPath) deployTokenInfo := gitprovider.DeployTokenInfo{Name: name} deployToken, changed, err := dts.Reconcile(ctx, deployTokenInfo) if err != nil { return nil, err } if changed { b.logger.Successf("configured deploy token %q for %q", deployTokenInfo.Name, b.repository.Repository().String()) deployTokenInfo := deployToken.Get() return &deployTokenInfo, nil } b.logger.Successf("reconciled deploy token for source secret") return nil, nil } // reconcileOrgRepository reconciles a gitprovider.OrgRepository // with the GitProviderBootstrapper values, including any // gitprovider.TeamAccessInfo configurations. // // If one of the gitprovider.TeamAccessInfo does not reconcile // successfully, the gitprovider.UserRepository and an // ErrReconciledWithWarning error are returned. func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (gitprovider.UserRepository, error) { b.logger.Actionf("connecting to %s", b.provider.SupportedDomain()) // Construct the repository and other configuration objects // go-git-provider likes to work with subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) orgRef, err := b.getOrganization(ctx, subOrgs) if err != nil { 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) // Reconcile the organization repository repo, err := b.provider.OrgRepositories().Get(ctx, repoRef) if err != nil { if !errors.Is(err, gitprovider.ErrNotFound) { return nil, fmt.Errorf("failed to get Git repository %q: provider error: %w", repoRef.String(), err) } // go-git-providers has at present some issues with the idempotency // of the available Reconcile methods, and setting e.g. the default // branch correctly. Resort to Create until this has been resolved. repo, err = b.provider.OrgRepositories().Create(ctx, repoRef, repoInfo) if err != nil { return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err) } b.logger.Successf("repository %q created", repoRef.String()) } var changed bool if b.reconcile { if err = retry(1, 2*time.Second, func() (err error) { changed, err = repo.Reconcile(ctx) return }); err != nil { return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err) } if changed { b.logger.Successf("repository %q reconciled", repoRef.String()) } } // Build the team access config teamAccessInfo, err := buildTeamAccessInfo(b.teams, gitprovider.RepositoryPermissionVar(gitprovider.RepositoryPermissionMaintain)) if err != nil { return nil, fmt.Errorf("failed to reconcile repository team access: %w", err) } // Reconcile the team access config on best effort (that being: // record the error as a warning, but continue with the // reconciliation of the others) var warning error if count := len(teamAccessInfo); count > 0 { b.logger.Actionf("reconciling repository permissions") for _, i := range teamAccessInfo { var err error // Don't reconcile team if team already exists and b.reconcile is false if team, err := repo.TeamAccess().Get(ctx, i.Name); err == nil && !b.reconcile && team != nil { continue } _, changed, err = repo.TeamAccess().Reconcile(ctx, i) if err != nil { warning = fmt.Errorf("failed to grant permissions to team: %w", ErrReconciledWithWarning) b.logger.Failuref("failed to grant %q permissions to %q: %s", *i.Permission, i.Name, err.Error()) } else if changed { b.logger.Successf("granted %q permissions to %q", *i.Permission, i.Name) } } b.logger.Successf("reconciled repository permissions") } return repo, warning } // reconcileUserRepository reconciles a gitprovider.UserRepository // with the GitProviderBootstrapper values. It returns the reconciled // gitprovider.UserRepository, or an error. func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) (gitprovider.UserRepository, error) { b.logger.Actionf("connecting to %s", b.provider.SupportedDomain()) // Construct the repository and other metadata objects // go-git-provider likes to work with. _, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) userRef := newUserRef(b.provider.SupportedDomain(), b.owner) repoRef := newUserRepositoryRef(userRef, repoName) repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility) // Reconcile the user repository repo, err := b.provider.UserRepositories().Get(ctx, repoRef) if err != nil { if !errors.Is(err, gitprovider.ErrNotFound) { return nil, fmt.Errorf("failed to get Git repository %q: provider error: %w", repoRef.String(), err) } // go-git-providers has at present some issues with the idempotency // of the available Reconcile methods, and setting e.g. the default // branch correctly. Resort to Create until this has been resolved. repo, err = b.provider.UserRepositories().Create(ctx, repoRef, repoInfo) if err != nil { var userErr *gitprovider.ErrIncorrectUser if errors.As(err, &userErr) { // return a better error message when the wrong owner is set err = fmt.Errorf("the specified owner '%s' doesn't match the identity associated with the given token", b.owner) } return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err) } b.logger.Successf("repository %q created", repoRef.String()) } if b.reconcile { var changed bool if err = retry(1, 2*time.Second, func() (err error) { changed, err = repo.Reconcile(ctx) return }); err != nil { return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err) } if changed { b.logger.Successf("repository %q reconciled", repoRef.String()) } } return repo, nil } // getOrganization retrieves and returns the gitprovider.Organization // using the GitProviderBootstrapper values. func (b *GitProviderBootstrapper) getOrganization(ctx context.Context, subOrgs []string) (*gitprovider.OrganizationRef, error) { orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs) // With Stash get the organization to be sure to get the correct key if string(b.provider.ProviderID()) == string(provider.GitProviderStash) { org, err := b.provider.Organizations().Get(ctx, orgRef) if err != nil { return nil, fmt.Errorf("failed to get Git organization: %w", err) } orgRef = org.Organization() return &orgRef, nil } return &orgRef, nil } // getCloneURL returns the Git clone URL for the given // gitprovider.UserRepository. If the given transport type is // gitprovider.TransportTypeSSH and a custom SSH hostname is configured, // the hostname of the URL will be modified to this hostname. func (b *GitProviderBootstrapper) getCloneURL(repository gitprovider.UserRepository, transport gitprovider.TransportType) (string, error) { var url string if cloner, ok := repository.(gitprovider.CloneableURL); ok { url = cloner.GetCloneURL("", transport) } else { url = repository.Repository().GetCloneURL(transport) } var err error if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" { if url, err = setHostname(url, b.sshHostname); err != nil { err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", url, err) } } return url, err } // splitSubOrganizationsFromRepositoryName removes any prefixed sub // organizations from the given repository name by splitting the // string into a slice by '/'. // The last (or only) item of the slice result is assumed to be the // repository name, other items (nested) sub organizations. func splitSubOrganizationsFromRepositoryName(name string) ([]string, string) { elements := strings.Split(name, "/") i := len(elements) switch i { case 1: return nil, name default: return elements[:i-1], elements[i-1] } } // buildTeamAccessInfo constructs a gitprovider.TeamAccessInfo slice // from the given string map of team names to permissions. // // Providing a default gitprovider.RepositoryPermission is optional, // and omitting it will make it default to the go-git-provider default. // // An error is returned if any of the given permissions is invalid. func buildTeamAccessInfo(m map[string]string, defaultPermissions *gitprovider.RepositoryPermission) ([]gitprovider.TeamAccessInfo, error) { var infos []gitprovider.TeamAccessInfo if defaultPermissions != nil { if err := gitprovider.ValidateRepositoryPermission(*defaultPermissions); err != nil { return nil, fmt.Errorf("invalid default team permission %q", *defaultPermissions) } } for n, p := range m { permission := defaultPermissions if p != "" { p := gitprovider.RepositoryPermission(p) if err := gitprovider.ValidateRepositoryPermission(p); err != nil { return nil, fmt.Errorf("invalid permission %q for team %q", p, n) } permission = &p } i := gitprovider.TeamAccessInfo{ Name: n, Permission: permission, } infos = append(infos, i) } return infos, nil } // newOrganizationRef constructs a gitprovider.OrganizationRef with the // given values and returns the result. func newOrganizationRef(domain, organization string, subOrganizations []string) gitprovider.OrganizationRef { return gitprovider.OrganizationRef{ Domain: domain, Organization: organization, SubOrganizations: subOrganizations, } } // newOrgRepositoryRef constructs a gitprovider.OrgRepositoryRef with // the given values and returns the result. func newOrgRepositoryRef(organizationRef gitprovider.OrganizationRef, name string) gitprovider.OrgRepositoryRef { return gitprovider.OrgRepositoryRef{ OrganizationRef: organizationRef, RepositoryName: name, } } // newUserRef constructs a gitprovider.UserRef with the given values // and returns the result. func newUserRef(domain, login string) gitprovider.UserRef { return gitprovider.UserRef{ Domain: domain, UserLogin: login, } } // newUserRepositoryRef constructs a gitprovider.UserRepositoryRef with // the given values and returns the result. func newUserRepositoryRef(userRef gitprovider.UserRef, name string) gitprovider.UserRepositoryRef { return gitprovider.UserRepositoryRef{ UserRef: userRef, RepositoryName: name, } } // newRepositoryInfo constructs a gitprovider.RepositoryInfo with the // given values and returns the result. func newRepositoryInfo(description, defaultBranch, visibility string) gitprovider.RepositoryInfo { var i gitprovider.RepositoryInfo if description != "" { i.Description = gitprovider.StringVar(description) } if defaultBranch != "" { i.DefaultBranch = gitprovider.StringVar(defaultBranch) } if visibility != "" { i.Visibility = gitprovider.RepositoryVisibilityVar(gitprovider.RepositoryVisibility(visibility)) } return i } // newDeployKeyInfo constructs a gitprovider.DeployKeyInfo with the // given values and returns the result. func newDeployKeyInfo(name, publicKey string, readWrite bool) gitprovider.DeployKeyInfo { keyInfo := gitprovider.DeployKeyInfo{ Name: name, Key: []byte(publicKey), } if readWrite { keyInfo.ReadOnly = gitprovider.BoolVar(false) } return keyInfo } func deployKeyName(namespace, secretName, branch, path string) string { var name string for _, v := range []string{namespace, secretName, branch, path} { if v == "" { continue } if name == "" { name = v } else { name = name + "-" + v } } return name } func deployTokenName(namespace, secretName, branch, path string) string { var elems []string for _, v := range []string{namespace, secretName, branch, path} { if v == "" { continue } elems = append(elems, v) } return strings.Join(elems, "-") } // setHostname is a helper to replace the hostname of the given URL. // TODO(hidde): support for this should be added in go-git-providers. func setHostname(URL, hostname string) (string, error) { u, err := url.Parse(URL) if err != nil { return URL, err } u.Host = hostname return u.String(), nil } ================================================ FILE: pkg/bootstrap/bootstrap_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bootstrap import ( "context" "testing" "github.com/fluxcd/pkg/apis/meta" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/internal/utils" ) func Test_hasRevision(t *testing.T) { var revision = "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a" tests := []struct { name string obj objectWithConditions rev string expectErr bool expectedBool bool }{ { name: "Kustomization revision", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, }, Status: kustomizev1.KustomizationStatus{ LastAttemptedRevision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", }, }, expectedBool: true, }, { name: "GitRepository revision", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, Status: sourcev1.GitRepositoryStatus{ Artifact: &meta.Artifact{ Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", }, }, }, expectedBool: true, }, { name: "GitRepository revision (wrong revision)", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, Status: sourcev1.GitRepositoryStatus{ Artifact: &meta.Artifact{ Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", }, }, }, }, { name: "Kustomization revision (empty revision)", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, }, Status: kustomizev1.KustomizationStatus{ LastAttemptedRevision: "", }, }, }, { name: "OCIRepository revision", obj: &sourcev1.OCIRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.OCIRepositoryKind, }, Status: sourcev1.OCIRepositoryStatus{ Artifact: &meta.Artifact{ Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", }, }, }, expectedBool: true, }, { name: "Alert revision(Not supported)", obj: ¬ificationv1.Alert{ TypeMeta: metav1.TypeMeta{ Kind: notificationv1.AlertKind, }, Status: notificationv1.AlertStatus{ ObservedGeneration: 1, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.obj) g.Expect(err).To(BeNil()) got, err := hasRevision(tt.obj.GetObjectKind().GroupVersionKind().Kind, obj, revision) if tt.expectErr { g.Expect(err).To(HaveOccurred()) return } g.Expect(got).To(Equal(tt.expectedBool)) }) } } func Test_objectReconciled(t *testing.T) { expectedRev := "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a" type updateStatus struct { statusFn func(o client.Object) expectedErr bool expectedBool bool } tests := []struct { name string obj objectWithConditions statuses []updateStatus }{ { name: "GitRepository with no status", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "suspended Kustomization", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, APIVersion: kustomizev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", }, Spec: kustomizev1.KustomizationSpec{ Suspend: true, }, }, statuses: []updateStatus{ { expectedErr: true, }, }, }, { name: "Kustomization - status with old generation", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, APIVersion: kustomizev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: kustomizev1.KustomizationStatus{ ObservedGeneration: -1, }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "GitRepository - status with same generation but no conditions", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "GitRepository - status with conditions but no ready condition", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReconcilingCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Progressing", Message: "Progressing"}, }, }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "Kustomization - status with false ready condition", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, APIVersion: kustomizev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: kustomizev1.KustomizationStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Failing", Message: "Failed to clone"}, }, }, }, statuses: []updateStatus{ { expectedErr: true, expectedBool: false, }, }, }, { name: "Kustomization - status with true ready condition but different revision", obj: &kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: kustomizev1.KustomizationKind, APIVersion: kustomizev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: kustomizev1.KustomizationStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Passing", Message: "Applied revision"}, }, LastAttemptedRevision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "GitRepository - status with true ready condition but different revision", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, }, Artifact: &meta.Artifact{ Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", }, }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: false, }, }, }, { name: "GitRepository - ready with right revision", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, Status: sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, }, Artifact: &meta.Artifact{ Revision: expectedRev, }, }, }, statuses: []updateStatus{ { expectedErr: false, expectedBool: true, }, }, }, { name: "GitRepository - sequence of status updates before ready", obj: &sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: sourcev1.GitRepositoryKind, APIVersion: sourcev1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", Namespace: "flux-system", Generation: 1, }, }, statuses: []updateStatus{ { // observed gen different statusFn: func(o client.Object) { gitRepo := o.(*sourcev1.GitRepository) gitRepo.Status = sourcev1.GitRepositoryStatus{ ObservedGeneration: -1, } }, }, { // ready failing statusFn: func(o client.Object) { gitRepo := o.(*sourcev1.GitRepository) gitRepo.Status = sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Not Ready", Message: "Transient connection issue"}, }, } }, expectedErr: true, }, { // updated to a different revision statusFn: func(o client.Object) { gitRepo := o.(*sourcev1.GitRepository) gitRepo.Status = sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, }, Artifact: &meta.Artifact{ Revision: "wrong rev", }, } }, }, { // updated to the expected revision statusFn: func(o client.Object) { gitRepo := o.(*sourcev1.GitRepository) gitRepo.Status = sourcev1.GitRepositoryStatus{ ObservedGeneration: 1, Conditions: []metav1.Condition{ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, }, Artifact: &meta.Artifact{ Revision: expectedRev, }, } }, expectedBool: true, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) builder.WithObjects(tt.obj) kubeClient := builder.Build() for _, updates := range tt.statuses { if updates.statusFn != nil { updates.statusFn(tt.obj) cloneObj := tt.obj.DeepCopyObject().(client.Object) g.Expect(kubeClient.Update(context.TODO(), cloneObj)).To(Succeed()) } waitFunc := objectReconciled(kubeClient, client.ObjectKeyFromObject(tt.obj), tt.obj, expectedRev) got, err := waitFunc(context.TODO()) g.Expect(err != nil).To(Equal(updates.expectedErr), "unexpected error: %v, for: %v", err, tt.obj) g.Expect(got).To(Equal(updates.expectedBool)) } }) } } ================================================ FILE: pkg/bootstrap/options.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bootstrap import ( "fmt" "os" "k8s.io/cli-runtime/pkg/genericclioptions" "github.com/ProtonMail/go-crypto/openpgp" "github.com/fluxcd/pkg/git" runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/flux2/v2/pkg/log" ) type Option interface { GitOption GitProviderOption } func WithBranch(branch string) Option { return branchOption(branch) } type branchOption string func (o branchOption) applyGit(b *PlainGitBootstrapper) { b.branch = string(o) } func (o branchOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } func WithSignature(name, email string) Option { return signatureOption{ Name: name, Email: email, } } type signatureOption git.Signature func (o signatureOption) applyGit(b *PlainGitBootstrapper) { if o.Name != "" { b.signature.Name = o.Name } if o.Email != "" { b.signature.Email = o.Email } } func (o signatureOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } func WithCommitMessageAppendix(appendix string) Option { return commitMessageAppendixOption(appendix) } type commitMessageAppendixOption string func (o commitMessageAppendixOption) applyGit(b *PlainGitBootstrapper) { b.commitMessageAppendix = string(o) } func (o commitMessageAppendixOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } func WithKubeconfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) Option { return kubeconfigOption{ rcg: rcg, opts: opts, } } type kubeconfigOption struct { rcg genericclioptions.RESTClientGetter opts *runclient.Options } func (o kubeconfigOption) applyGit(b *PlainGitBootstrapper) { b.restClientGetter = o.rcg b.restClientOptions = o.opts } func (o kubeconfigOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } func WithLogger(logger log.Logger) Option { return loggerOption{logger} } type loggerOption struct { logger log.Logger } func (o loggerOption) applyGit(b *PlainGitBootstrapper) { b.logger = o.logger } func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) { b.logger = o.logger } func WithGitCommitSigning(gpgKeyRing openpgp.EntityList, passphrase, keyID string) Option { return gitCommitSigningOption{ gpgKeyRing: gpgKeyRing, gpgPassphrase: passphrase, gpgKeyID: keyID, } } type gitCommitSigningOption struct { gpgKeyRing openpgp.EntityList gpgPassphrase string gpgKeyID string } func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) { b.gpgKeyRing = o.gpgKeyRing b.gpgPassphrase = o.gpgPassphrase b.gpgKeyID = o.gpgKeyID } func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } func LoadEntityListFromPath(path string) (openpgp.EntityList, error) { if path == "" { return nil, nil } r, err := os.Open(path) if err != nil { return nil, fmt.Errorf("unable to open GPG key ring: %w", err) } entityList, err := openpgp.ReadKeyRing(r) if err != nil { return nil, fmt.Errorf("unable to read GPG key ring: %w", err) } return entityList, nil } ================================================ FILE: pkg/bootstrap/provider/factory.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package provider import ( "fmt" "github.com/fluxcd/go-git-providers/gitea" "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitlab" "github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/go-git-providers/stash" ) // BuildGitProvider builds a gitprovider.Client for the provided // Config. It returns an error if the Config.Provider // is not supported, or if the construction of the client fails. func BuildGitProvider(config Config) (gitprovider.Client, error) { var client gitprovider.Client var err error switch config.Provider { case GitProviderGitHub: opts := []gitprovider.ClientOption{ gitprovider.WithOAuth2Token(config.Token), } if config.Hostname != "" { opts = append(opts, gitprovider.WithDomain(config.Hostname)) } if config.CaBundle != nil { opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle)) } if client, err = github.NewClient(opts...); err != nil { return nil, err } case GitProviderGitea: opts := []gitprovider.ClientOption{} if config.Hostname != "" { opts = append(opts, gitprovider.WithDomain(config.Hostname)) } if config.CaBundle != nil { opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle)) } if client, err = gitea.NewClient(config.Token, opts...); err != nil { return nil, err } case GitProviderGitLab: opts := []gitprovider.ClientOption{ gitprovider.WithConditionalRequests(true), } if config.Hostname != "" { opts = append(opts, gitprovider.WithDomain(config.Hostname)) } if config.CaBundle != nil { opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle)) } if client, err = gitlab.NewClient(config.Token, "", opts...); err != nil { return nil, err } case GitProviderStash: opts := []gitprovider.ClientOption{} if config.Hostname != "" { opts = append(opts, gitprovider.WithDomain(config.Hostname)) } if config.CaBundle != nil { opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle)) } if client, err = stash.NewStashClient(config.Username, config.Token, opts...); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported Git provider '%s'", config.Provider) } return client, err } ================================================ FILE: pkg/bootstrap/provider/provider.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package provider // GitProvider holds a Git provider definition. type GitProvider string const ( GitProviderGitHub GitProvider = "github" GitProviderGitea GitProvider = "gitea" GitProviderGitLab GitProvider = "gitlab" GitProviderStash GitProvider = "stash" ) // Config defines the configuration for connecting to a GitProvider. type Config struct { // Provider defines the GitProvider. Provider GitProvider // Hostname is the HTTP/S hostname of the Provider, // e.g. github.example.com. Hostname string // Username contains the username used to authenticate with // the Provider. Username string // Token contains the token used to authenticate with the // Provider. Token string // CABunle contains the CA bundle to use for the client. CaBundle []byte } ================================================ FILE: pkg/log/log.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package log type Logger interface { // Actionf logs a formatted action message. Actionf(format string, a ...interface{}) // Generatef logs a formatted generate message. Generatef(format string, a ...interface{}) // Waitingf logs a formatted waiting message. Waitingf(format string, a ...interface{}) // Successf logs a formatted success message. Successf(format string, a ...interface{}) // Warningf logs a formatted warning message. Warningf(format string, a ...interface{}) // Failuref logs a formatted failure message. Failuref(format string, a ...interface{}) } ================================================ FILE: pkg/log/nop.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package log type NopLogger struct{} func (NopLogger) Actionf(format string, a ...interface{}) {} func (NopLogger) Generatef(format string, a ...interface{}) {} func (NopLogger) Waitingf(format string, a ...interface{}) {} func (NopLogger) Successf(format string, a ...interface{}) {} func (NopLogger) Warningf(format string, a ...interface{}) {} func (NopLogger) Failuref(format string, a ...interface{}) {} ================================================ FILE: pkg/manifestgen/doc.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package manifestgen generates Kubernetes manifests for flux install // and the Git source and Kustomization manifests for flux bootstrap. package manifestgen ================================================ FILE: pkg/manifestgen/install/install.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package install import ( "context" "encoding/json" "fmt" "net/http" "os" "path" "strings" "time" securejoin "github.com/cyphar/filepath-securejoin" "github.com/hashicorp/go-cleanhttp" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) // Generate returns the install manifests as a multi-doc YAML. // The manifests are built from a GitHub release or from a // Kustomize overlay if the supplied Options.BaseURL is a local path. // The manifestsBase should be set to an empty string when Generate is // called by consumers that don't embed the manifests. func Generate(options Options, manifestsBase string) (*manifestgen.Manifest, error) { ctx, cancel := context.WithTimeout(context.Background(), options.Timeout) defer cancel() var err error output, err := securejoin.SecureJoin(manifestsBase, options.ManifestFile) if err != nil { return nil, err } if !strings.HasPrefix(options.BaseURL, "http") { if err := build(options.BaseURL, output); err != nil { return nil, err } } else { // download the manifests base from GitHub if manifestsBase == "" { manifestsBase, err = manifestgen.MkdirTempAbs("", options.Namespace) if err != nil { return nil, fmt.Errorf("temp dir error: %w", err) } defer os.RemoveAll(manifestsBase) output, err = securejoin.SecureJoin(manifestsBase, options.ManifestFile) if err != nil { return nil, err } if err := fetch(ctx, options.BaseURL, options.Version, manifestsBase); err != nil { return nil, err } } if err := generate(manifestsBase, options); err != nil { return nil, err } if err := build(manifestsBase, output); err != nil { return nil, err } } content, err := os.ReadFile(output) if err != nil { return nil, err } return &manifestgen.Manifest{ Path: path.Join(options.TargetPath, options.Namespace, options.ManifestFile), Content: fmt.Sprintf("%s\n%s", GetGenWarning(options), string(content)), }, nil } // GetLatestVersion calls the GitHub API and returns the latest released version. func GetLatestVersion() (string, error) { ghURL := "https://api.github.com/repos/fluxcd/flux2/releases/latest" c := cleanhttp.DefaultClient() c.Timeout = 15 * time.Second res, err := c.Get(ghURL) if err != nil { return "", fmt.Errorf("GitHub API call failed: %w", err) } if res.Body != nil { defer res.Body.Close() } type meta struct { Tag string `json:"tag_name"` } var m meta if err := json.NewDecoder(res.Body).Decode(&m); err != nil { return "", fmt.Errorf("decoding GitHub API response failed: %w", err) } return m.Tag, err } // ExistingVersion calls the GitHub API to confirm the given version does exist. func ExistingVersion(version string) (bool, error) { if !strings.HasPrefix(version, "v") { version = "v" + version } ghURL := fmt.Sprintf("https://api.github.com/repos/fluxcd/flux2/releases/tags/%s", version) c := cleanhttp.DefaultClient() c.Timeout = 15 * time.Second res, err := c.Get(ghURL) if err != nil { return false, fmt.Errorf("GitHub API call failed: %w", err) } if res.Body != nil { defer res.Body.Close() } switch res.StatusCode { case http.StatusOK: return true, nil case http.StatusNotFound: return false, nil default: return false, fmt.Errorf("GitHub API returned an unexpected status code (%d)", res.StatusCode) } } // GetGenWarning generates a consistent generation warning in the install and bootstrap case in the following format: // # This manifest was generated by flux. DO NOT EDIT. // # Flux version: v0.21.1 // # Components: source-controller,kustomize-controller,helm-controller,notification-controller,image-reflector-controller,image-automation-controller func GetGenWarning(options Options) string { return fmt.Sprintf("---\n%s\n# Flux Version: %s\n# Components: %s", manifestgen.GenWarning, options.Version, strings.Join(options.Components, ",")) } ================================================ FILE: pkg/manifestgen/install/install_test.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package install import ( "fmt" "strings" "testing" ) func TestGenerate(t *testing.T) { opts := MakeDefaultOptions() opts.TolerationKeys = []string{"node.kubernetes.io/controllers"} output, err := Generate(opts, "") if err != nil { t.Fatal(err) } for _, component := range opts.Components { img := fmt.Sprintf("%s/%s", opts.Registry, component) if !strings.Contains(output.Content, img) { t.Errorf("component image '%s' not found", img) } } if !strings.Contains(output.Content, opts.TolerationKeys[0]) { t.Errorf("toleration key '%s' not found", opts.TolerationKeys[0]) } warning := GetGenWarning(opts) if !strings.HasPrefix(output.Content, warning) { t.Errorf("Generation warning '%s' not found", warning) } t.Log(output) } ================================================ FILE: pkg/manifestgen/install/manifests.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package install import ( "bytes" "context" "fmt" "net/http" "os" "path" "path/filepath" "strings" "github.com/hashicorp/go-cleanhttp" "github.com/fluxcd/pkg/kustomize/filesys" "github.com/fluxcd/pkg/tar" "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" ) func fetch(ctx context.Context, url, version, dir string) error { ghURL := fmt.Sprintf("%s/latest/download/manifests.tar.gz", url) if strings.HasPrefix(version, "v") { ghURL = fmt.Sprintf("%s/download/%s/manifests.tar.gz", url, version) } req, err := http.NewRequest("GET", ghURL, nil) if err != nil { return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err) } // download resp, err := cleanhttp.DefaultClient().Do(req.WithContext(ctx)) if err != nil { return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err) } defer resp.Body.Close() // check response if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to download manifests.tar.gz from %s, status: %s", ghURL, resp.Status) } // extract if err = tar.Untar(resp.Body, dir, tar.WithMaxUntarSize(-1)); err != nil { return fmt.Errorf("failed to untar manifests.tar.gz from %s, error: %w", ghURL, err) } return nil } func generate(base string, options Options) error { if containsItemString(options.Components, options.NotificationController) { // We need to use full domain name here, as some users may deploy flux // in environments that use http proxy. // // In such environments they normally add `.cluster.local` and `.local` // suffixes to `no_proxy` variable in order to prevent cluster-local // traffic from going through http proxy. Without fully specified // domain they need to mention `notifications-controller` explicitly in // `no_proxy` variable after debugging http proxy logs. options.EventsAddr = fmt.Sprintf("http://%s.$(RUNTIME_NAMESPACE).svc.%s./", options.NotificationController, options.ClusterDomain) } if err := execTemplate(options, namespaceTmpl, path.Join(base, "namespace.yaml")); err != nil { return fmt.Errorf("generate namespace failed: %w", err) } if err := execTemplate(options, labelsTmpl, path.Join(base, "labels.yaml")); err != nil { return fmt.Errorf("generate labels failed: %w", err) } if err := execTemplate(options, nodeSelectorTmpl, path.Join(base, "node-selector.yaml")); err != nil { return fmt.Errorf("generate node selector failed: %w", err) } if err := execTemplate(options, kustomizationTmpl, path.Join(base, "kustomization.yaml")); err != nil { return fmt.Errorf("generate kustomization failed: %w", err) } if err := os.MkdirAll(path.Join(base, "roles"), os.ModePerm); err != nil { return fmt.Errorf("generate roles failed: %w", err) } if err := execTemplate(options, kustomizationRolesTmpl, path.Join(base, "roles/kustomization.yaml")); err != nil { return fmt.Errorf("generate roles kustomization failed: %w", err) } rbacFile := filepath.Join(base, "roles/rbac.yaml") if err := copyFile(filepath.Join(base, "rbac.yaml"), rbacFile); err != nil { return fmt.Errorf("generate rbac failed: %w", err) } // workaround for kustomize not being able to patch the SA in ClusterRoleBindings defaultNS := MakeDefaultOptions().Namespace if defaultNS != options.Namespace { rbac, err := os.ReadFile(rbacFile) if err != nil { return fmt.Errorf("reading rbac file failed: %w", err) } rbac = bytes.ReplaceAll(rbac, []byte(defaultNS), []byte(options.Namespace)) if err := os.WriteFile(rbacFile, rbac, os.ModePerm); err != nil { return fmt.Errorf("replacing service account namespace in rbac failed: %w", err) } } return nil } func build(base, output string) error { resources, err := kustomization.Build(base) if err != nil { return err } outputBase := filepath.Dir(strings.TrimSuffix(output, string(filepath.Separator))) fs, err := filesys.MakeFsOnDiskSecure(outputBase) if err != nil { return err } if err = fs.WriteFile(output, resources); err != nil { return err } return nil } ================================================ FILE: pkg/manifestgen/install/options.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package install import "time" type Options struct { BaseURL string Version string Namespace string Components []string ComponentsExtra []string EventsAddr string Registry string RegistryCredential string ImagePullSecret string WatchAllNamespaces bool NetworkPolicy bool LogLevel string NotificationController string ManifestFile string Timeout time.Duration TargetPath string ClusterDomain string TolerationKeys []string } func MakeDefaultOptions() Options { return Options{ Version: "latest", Namespace: "flux-system", Components: []string{"source-controller", "kustomize-controller", "helm-controller", "notification-controller"}, ComponentsExtra: []string{"image-reflector-controller", "image-automation-controller", "source-watcher"}, EventsAddr: "", Registry: "ghcr.io/fluxcd", RegistryCredential: "", ImagePullSecret: "", WatchAllNamespaces: true, NetworkPolicy: true, LogLevel: "info", BaseURL: "https://github.com/fluxcd/flux2/releases", NotificationController: "notification-controller", ManifestFile: "gotk-components.yaml", Timeout: time.Minute, TargetPath: "", ClusterDomain: "cluster.local", } } func containsItemString(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } ================================================ FILE: pkg/manifestgen/install/templates.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package install import ( "bufio" "bytes" "io" "os" "text/template" ) var kustomizationTmpl = `--- {{- $eventsAddr := .EventsAddr }} {{- $watchAllNamespaces := .WatchAllNamespaces }} {{- $registry := .Registry }} {{- $logLevel := .LogLevel }} {{- $clusterDomain := .ClusterDomain }} apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: {{.Namespace}} transformers: - labels.yaml resources: - namespace.yaml {{- if .NetworkPolicy }} - policies.yaml {{- end }} - roles {{- range .Components }} - {{.}}.yaml {{- end }} patches: - path: node-selector.yaml target: kind: Deployment {{- range $i, $component := .Components }} {{- if eq $component "notification-controller" }} - target: group: apps version: v1 kind: Deployment name: {{$component}} patch: |- - op: replace path: /spec/template/spec/containers/0/args/0 value: --watch-all-namespaces={{$watchAllNamespaces}} - op: replace path: /spec/template/spec/containers/0/args/1 value: --log-level={{$logLevel}} {{- else if or (eq $component "source-controller") (eq $component "source-watcher") }} - target: group: apps version: v1 kind: Deployment name: {{$component}} patch: |- - op: replace path: /spec/template/spec/containers/0/args/0 value: --events-addr={{$eventsAddr}} - op: replace path: /spec/template/spec/containers/0/args/1 value: --watch-all-namespaces={{$watchAllNamespaces}} - op: replace path: /spec/template/spec/containers/0/args/2 value: --log-level={{$logLevel}} - op: replace path: /spec/template/spec/containers/0/args/6 value: --storage-adv-addr={{$component}}.$(RUNTIME_NAMESPACE).svc.{{$clusterDomain}}. {{- else }} - target: group: apps version: v1 kind: Deployment name: {{$component}} patch: |- - op: replace path: /spec/template/spec/containers/0/args/0 value: --events-addr={{$eventsAddr}} - op: replace path: /spec/template/spec/containers/0/args/1 value: --watch-all-namespaces={{$watchAllNamespaces}} - op: replace path: /spec/template/spec/containers/0/args/2 value: --log-level={{$logLevel}} {{- end }} {{- end }} {{- range $i, $component := .Components }} {{- if eq $component "source-watcher" }} - target: kind: Deployment name: "(kustomize-controller|helm-controller)" patch: |- - op: add path: /spec/template/spec/containers/0/args/- value: --feature-gates=ExternalArtifact=true {{- end }} {{- end }} {{- if $registry }} images: {{- range $i, $component := .Components }} - name: fluxcd/{{$component}} newName: {{$registry}}/{{$component}} {{- end }} {{- end }} ` var kustomizationRolesTmpl = `--- apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: {{.Namespace}} resources: - rbac.yaml nameSuffix: -{{.Namespace}} ` var nodeSelectorTmpl = `--- apiVersion: apps/v1 kind: Deployment metadata: name: all spec: template: spec: nodeSelector: kubernetes.io/os: linux {{- if .ImagePullSecret }} imagePullSecrets: - name: {{.ImagePullSecret}} {{- end }} {{ if gt (len .TolerationKeys) 0 }} tolerations: {{- range $i, $key := .TolerationKeys }} - key: "{{$key}}" operator: "Exists" {{- end }} {{- end }} ` var labelsTmpl = `--- apiVersion: builtin kind: LabelTransformer metadata: name: labels labels: app.kubernetes.io/instance: {{.Namespace}} app.kubernetes.io/version: "{{.Version}}" app.kubernetes.io/part-of: flux fieldSpecs: - path: metadata/labels create: true - kind: Deployment path: spec/template/metadata/labels create: true ` var namespaceTmpl = `--- apiVersion: v1 kind: Namespace metadata: name: {{.Namespace}} labels: pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: latest ` func execTemplate(obj interface{}, tmpl, filename string) error { t, err := template.New("tmpl").Parse(tmpl) if err != nil { return err } var data bytes.Buffer writer := bufio.NewWriter(&data) if err := t.Execute(writer, obj); err != nil { return err } if err := writer.Flush(); err != nil { return err } file, err := os.Create(filename) if err != nil { return err } defer file.Close() _, err = io.WriteString(file, data.String()) if err != nil { return err } return file.Sync() } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return out.Close() } ================================================ FILE: pkg/manifestgen/kustomization/kustomization.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kustomization import ( "fmt" "os" "path/filepath" "strings" "sync" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/provider" kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/kustomize/filesys" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) // Generate scans the given directory for Kubernetes manifests and creates a // konfig.DefaultKustomizationFileName file, including all discovered manifests // as resources. func Generate(options Options) (*manifestgen.Manifest, error) { kfile := filepath.Join(options.TargetPath, konfig.DefaultKustomizationFileName()) abskfile := filepath.Join(options.BaseDir, kfile) scan := func(base string) ([]string, error) { var paths []string pvd := provider.NewDefaultDepProvider() rf := pvd.GetResourceFactory() err := options.FileSystem.Walk(base, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if path == base { return nil } if info.IsDir() { // If a sub-directory contains an existing Kustomization file, add the // directory as a resource and do not decent into it. for _, kfilename := range konfig.RecognizedKustomizationFileNames() { if options.FileSystem.Exists(filepath.Join(path, kfilename)) { paths = append(paths, path) return filepath.SkipDir } } return nil } fContents, err := options.FileSystem.ReadFile(path) if err != nil { return err } if _, err := rf.SliceFromBytes(fContents); err != nil { return nil } paths = append(paths, path) return nil }) return paths, err } if _, err := os.Stat(abskfile); err != nil { abs, err := filepath.Abs(filepath.Dir(abskfile)) if err != nil { return nil, err } files, err := scan(abs) if err != nil { return nil, err } f, err := options.FileSystem.Create(abskfile) if err != nil { return nil, err } if err = f.Close(); err != nil { return nil, err } kus := kustypes.Kustomization{ TypeMeta: kustypes.TypeMeta{ APIVersion: kustypes.KustomizationVersion, Kind: kustypes.KustomizationKind, }, } var resources []string for _, file := range files { relP, err := filepath.Rel(abs, file) if err != nil { return nil, err } resources = append(resources, relP) } kus.Resources = resources kd, err := yaml.Marshal(kus) if err != nil { return nil, err } return &manifestgen.Manifest{ Path: kfile, Content: string(kd), }, nil } kd, err := os.ReadFile(abskfile) if err != nil { return nil, err } return &manifestgen.Manifest{ Path: kfile, Content: string(kd), }, nil } // kustomizeBuildMutex is a workaround for a concurrent map read and map write bug. // TODO(stefan): https://github.com/kubernetes-sigs/kustomize/issues/3659 var kustomizeBuildMutex sync.Mutex // Build takes the path to a directory with a konfig.RecognizedKustomizationFileNames, // builds it, and returns the resulting manifests as multi-doc YAML. It restricts the // Kustomize file system to the parent directory of the base. func Build(base string) ([]byte, error) { // TODO(hidde): drop this when consumers have moved away to BuildWithRoot. parent := filepath.Dir(strings.TrimSuffix(base, string(filepath.Separator))) return BuildWithRoot(parent, base) } // BuildWithRoot takes the path to a directory with a konfig.RecognizedKustomizationFileNames, // builds it, and returns the resulting manifests as multi-doc YAML. // The Kustomize file system is restricted to root. func BuildWithRoot(root, base string) ([]byte, error) { kustomizeBuildMutex.Lock() defer kustomizeBuildMutex.Unlock() fs, err := filesys.MakeFsOnDiskSecureBuild(root) if err != nil { return nil, err } var kfile string for _, f := range konfig.RecognizedKustomizationFileNames() { if kf := filepath.Join(base, f); fs.Exists(kf) { kfile = kf break } } if kfile == "" { return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName()) } // Convert absolute paths to relative when possible, for kustomize // compatibility. If filepath.Rel fails (e.g. paths on different // Windows drives), keep the absolute path — kustomize handles // absolute paths correctly since go-getter was removed. // See: https://github.com/kubernetes-sigs/kustomize/issues/2789 // https://github.com/fluxcd/flux2/issues/1153 if filepath.IsAbs(base) { wd, err := os.Getwd() if err != nil { return nil, err } if relBase, err := filepath.Rel(wd, base); err == nil { base = relBase } } buildOptions := &krusty.Options{ LoadRestrictions: kustypes.LoadRestrictionsNone, PluginConfig: kustypes.DisabledPluginConfig(), } k := krusty.MakeKustomizer(buildOptions) m, err := k.Run(fs, base) if err != nil { return nil, err } resources, err := m.AsYaml() if err != nil { return nil, err } return resources, nil } ================================================ FILE: pkg/manifestgen/kustomization/options.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kustomization import "sigs.k8s.io/kustomize/kyaml/filesys" type Options struct { FileSystem filesys.FileSystem BaseDir string TargetPath string } func MakeDefaultOptions() Options { // TODO(hidde): switch MakeFsOnDisk to MakeFsOnDiskSecureBuild when we // break API. return Options{ FileSystem: filesys.MakeFsOnDisk(), BaseDir: "", TargetPath: "", } } ================================================ FILE: pkg/manifestgen/labels.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifestgen // These labels can be used to track down the namespace, custom resource definitions, deployments, // services, network policies, service accounts, cluster roles and cluster role bindings belonging to Flux. const ( PartOfLabelKey = "app.kubernetes.io/part-of" PartOfLabelValue = "flux" InstanceLabelKey = "app.kubernetes.io/instance" VersionLabelKey = "app.kubernetes.io/version" ) ================================================ FILE: pkg/manifestgen/manifest.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifestgen import ( "fmt" "os" "path/filepath" securejoin "github.com/cyphar/filepath-securejoin" ) const GenWarning = "# This manifest was generated by flux. DO NOT EDIT." // Manifest holds the data of a multi-doc YAML type Manifest struct { // Relative path to the YAML file Path string // Content in YAML format Content string } // WriteFile writes the YAML content to a file inside the root path. // If the file does not exist, WriteFile creates it with permissions perm, // otherwise WriteFile overwrites the file, without changing permissions. func (m *Manifest) WriteFile(rootDir string) (string, error) { output, err := securejoin.SecureJoin(rootDir, m.Path) if err != nil { return "", err } if err := os.MkdirAll(filepath.Dir(output), os.ModePerm); err != nil { return "", fmt.Errorf("unable to create dir, error: %w", err) } if err := os.WriteFile(output, []byte(m.Content), os.ModePerm); err != nil { return "", fmt.Errorf("unable to write file, error: %w", err) } return output, nil } ================================================ FILE: pkg/manifestgen/sourcesecret/options.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sourcesecret import ( "crypto/elliptic" "github.com/fluxcd/pkg/ssh" ) type PrivateKeyAlgorithm string const ( RSAPrivateKeyAlgorithm PrivateKeyAlgorithm = "rsa" ECDSAPrivateKeyAlgorithm PrivateKeyAlgorithm = "ecdsa" Ed25519PrivateKeyAlgorithm PrivateKeyAlgorithm = "ed25519" ) const ( AddressSecretKey = "address" UsernameSecretKey = "username" PasswordSecretKey = "password" CACrtSecretKey = "ca.crt" TLSCrtSecretKey = "tls.crt" TLSKeySecretKey = "tls.key" PrivateKeySecretKey = "identity" PublicKeySecretKey = "identity.pub" KnownHostsSecretKey = "known_hosts" BearerTokenKey = "bearerToken" TrustPolicyKey = "trustpolicy.json" // Deprecated: Replaced by CACrtSecretKey, but kept for backwards // compatibility with deprecated TLS flags. CAFileSecretKey = "caFile" // Deprecated: Replaced by TLSCrtSecretKey, but kept for backwards // compatibility with deprecated TLS flags. CertFileSecretKey = "certFile" // Deprecated: Replaced by TLSKeySecretKey, but kept for backwards // compatibility with deprecated TLS flags. KeyFileSecretKey = "keyFile" ) type Options struct { Name string Namespace string Labels map[string]string Registry string SSHHostname string PrivateKeyAlgorithm PrivateKeyAlgorithm RSAKeyBits int ECDSACurve elliptic.Curve Keypair *ssh.KeyPair Username string Password string CACrt []byte TLSCrt []byte TLSKey []byte TargetPath string ManifestFile string BearerToken string VerificationCrts []VerificationCrt TrustPolicy []byte Address string // GitHub App options GitHubAppID string GitHubAppInstallationOwner string GitHubAppInstallationID string GitHubAppPrivateKey string GitHubAppBaseURL string } type VerificationCrt struct { Name string CACrt []byte } func MakeDefaultOptions() Options { return Options{ Name: "flux-system", Namespace: "flux-system", Labels: map[string]string{}, PrivateKeyAlgorithm: RSAPrivateKeyAlgorithm, Username: "", Password: "", ManifestFile: "secret.yaml", BearerToken: "", } } ================================================ FILE: pkg/manifestgen/sourcesecret/sourcesecret.go ================================================ /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sourcesecret import ( "bytes" "encoding/base64" "encoding/json" "fmt" "net" "os" "path" "time" cryptssh "golang.org/x/crypto/ssh" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/runtime/secrets" "github.com/fluxcd/pkg/ssh" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) const defaultSSHPort = 22 // types gotten from https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/create/create_secret_docker.go#L64-L84 // DockerConfigJSON represents a local docker auth config file // for pulling images. type DockerConfigJSON struct { Auths DockerConfig `json:"auths"` } // DockerConfig represents the config file used by the docker CLI. // This config that represents the credentials that should be used // when pulling images from specific image repositories. type DockerConfig map[string]DockerConfigEntry // DockerConfigEntry holds the user information that grant the access to docker registry type DockerConfigEntry struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Email string `json:"email,omitempty"` Auth string `json:"auth,omitempty"` } func GenerateGit(options Options) (*manifestgen.Manifest, error) { var err error var keypair *ssh.KeyPair switch { case options.Username != "" && options.Password != "": // noop case options.Keypair != nil: keypair = options.Keypair case len(options.PrivateKeyAlgorithm) > 0: if keypair, err = generateKeyPair(options); err != nil { return nil, err } } var hostKey []byte if keypair != nil { if hostKey, err = ScanHostKey(options.SSHHostname); err != nil { return nil, err } } secret := buildGitSecret(keypair, hostKey, options) return secretToManifest(&secret, options) } func GenerateTLS(options Options) (*manifestgen.Manifest, error) { var opts []secrets.TLSSecretOption if len(options.TLSCrt) > 0 || len(options.TLSKey) > 0 { opts = append(opts, secrets.WithCertKeyPair(options.TLSCrt, options.TLSKey)) } if len(options.CACrt) > 0 { opts = append(opts, secrets.WithCAData(options.CACrt)) } secret, err := secrets.MakeTLSSecret(options.Name, options.Namespace, opts...) if err != nil { return nil, err } secret.Labels = options.Labels return secretToManifest(secret, options) } func GenerateOCI(options Options) (*manifestgen.Manifest, error) { secret, err := secrets.MakeRegistrySecret( options.Name, options.Namespace, options.Registry, options.Username, options.Password, ) if err != nil { return nil, err } secret.Labels = options.Labels return secretToManifest(secret, options) } func GenerateHelm(options Options) (*manifestgen.Manifest, error) { hasBasicAuth := options.Username != "" || options.Password != "" hasClientCert := len(options.TLSCrt) > 0 || len(options.TLSKey) > 0 hasCACert := len(options.CACrt) > 0 var secret *corev1.Secret var err error switch { case hasClientCert: // Priority 1: Client certificate (mTLS) - highest priority like CertSecretRef var opts []secrets.TLSSecretOption opts = append(opts, secrets.WithCertKeyPair(options.TLSCrt, options.TLSKey)) if hasCACert { opts = append(opts, secrets.WithCAData(options.CACrt)) } secret, err = secrets.MakeTLSSecret(options.Name, options.Namespace, opts...) if err != nil { return nil, err } case hasBasicAuth: // Priority 2: Basic authentication (can include CA certificate) secret, err = secrets.MakeBasicAuthSecret( options.Name, options.Namespace, options.Username, options.Password, ) if err != nil { return nil, err } // Add CA certificate to BasicAuth secret for HTTPS repositories with custom CA // (e.g., self-signed certificates or internal certificate authorities) if hasCACert { if secret.StringData == nil { secret.StringData = make(map[string]string) } secret.StringData[CACrtSecretKey] = string(options.CACrt) } case hasCACert: // Priority 3: CA certificate only var opts []secrets.TLSSecretOption opts = append(opts, secrets.WithCAData(options.CACrt)) secret, err = secrets.MakeTLSSecret(options.Name, options.Namespace, opts...) if err != nil { return nil, err } default: // No authentication credentials provided - create empty secret for backward compatibility secret = &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: options.Name, Namespace: options.Namespace, }, StringData: map[string]string{}, } } secret.Labels = options.Labels return secretToManifest(secret, options) } func GenerateProxy(options Options) (*manifestgen.Manifest, error) { secret, err := secrets.MakeProxySecret( options.Name, options.Namespace, options.Address, options.Username, options.Password, ) if err != nil { return nil, err } secret.Labels = options.Labels return secretToManifest(secret, options) } func GenerateNotation(options Options) (*manifestgen.Manifest, error) { secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: options.Name, Namespace: options.Namespace, Labels: options.Labels, }, StringData: map[string]string{}, } for _, crt := range options.VerificationCrts { secret.StringData[crt.Name] = string(crt.CACrt) } if len(options.TrustPolicy) > 0 { secret.StringData[TrustPolicyKey] = string(options.TrustPolicy) } return secretToManifest(secret, options) } func GenerateGitHubApp(options Options) (*manifestgen.Manifest, error) { var opts []secrets.GitHubAppOption if owner := options.GitHubAppInstallationOwner; owner != "" { opts = append(opts, secrets.WithGitHubAppInstallationOwner(owner)) } if id := options.GitHubAppInstallationID; id != "" { opts = append(opts, secrets.WithGitHubAppInstallationID(id)) } if u := options.GitHubAppBaseURL; u != "" { opts = append(opts, secrets.WithGitHubAppBaseURL(u)) } secret, err := secrets.MakeGitHubAppSecret( options.Name, options.Namespace, options.GitHubAppID, options.GitHubAppPrivateKey, opts..., ) if err != nil { return nil, err } secret.Labels = options.Labels return secretToManifest(secret, options) } func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) { if path == "" { return nil, nil } b, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to open private key file: %w", err) } return LoadKeyPair(b, password) } func LoadKeyPair(privateKey []byte, password string) (*ssh.KeyPair, error) { var ppk cryptssh.Signer var err error if password != "" { ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password)) } else { ppk, err = cryptssh.ParsePrivateKey(privateKey) } if err != nil { return nil, err } return &ssh.KeyPair{ PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), PrivateKey: privateKey, }, nil } func buildGitSecret(keypair *ssh.KeyPair, hostKey []byte, options Options) (secret corev1.Secret) { secret.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", } secret.ObjectMeta = metav1.ObjectMeta{ Name: options.Name, Namespace: options.Namespace, } secret.Labels = options.Labels secret.StringData = map[string]string{} if options.Username != "" && options.Password != "" { secret.StringData[UsernameSecretKey] = options.Username secret.StringData[PasswordSecretKey] = options.Password } if options.BearerToken != "" { secret.StringData[BearerTokenKey] = options.BearerToken } if len(options.CACrt) != 0 { secret.StringData[CACrtSecretKey] = string(options.CACrt) } // SSH keypair (identity + identity.pub + known_hosts) if keypair != nil && len(hostKey) != 0 { secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) // set password if present if options.Password != "" { secret.StringData[PasswordSecretKey] = string(options.Password) } } return secret } func secretToManifest(secret *corev1.Secret, options Options) (*manifestgen.Manifest, error) { b, err := yaml.Marshal(secret) if err != nil { return nil, err } return &manifestgen.Manifest{ Path: path.Join(options.TargetPath, options.Namespace, options.ManifestFile), Content: fmt.Sprintf("---\n%s", resourceToString(b)), }, nil } func generateKeyPair(options Options) (*ssh.KeyPair, error) { var keyGen ssh.KeyPairGenerator switch options.PrivateKeyAlgorithm { case RSAPrivateKeyAlgorithm: keyGen = ssh.NewRSAGenerator(options.RSAKeyBits) case ECDSAPrivateKeyAlgorithm: keyGen = ssh.NewECDSAGenerator(options.ECDSACurve) case Ed25519PrivateKeyAlgorithm: keyGen = ssh.NewEd25519Generator() default: return nil, fmt.Errorf("unsupported public key algorithm: %s", options.PrivateKeyAlgorithm) } pair, err := keyGen.Generate() if err != nil { return nil, fmt.Errorf("key pair generation failed, error: %w", err) } return pair, nil } func ScanHostKey(host string) ([]byte, error) { if _, _, err := net.SplitHostPort(host); err != nil { // Assume we are dealing with a hostname without a port, // append the default SSH port as this is required for // host key scanning to work. host = fmt.Sprintf("%s:%d", host, defaultSSHPort) } hostKey, err := ssh.ScanHostKey(host, 30*time.Second, []string{}, false) if err != nil { return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err) } return bytes.TrimSpace(hostKey), nil } func resourceToString(data []byte) string { data = bytes.Replace(data, []byte(" creationTimestamp: null\n"), []byte(""), 1) data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) return string(data) } func GenerateDockerConfigJson(url, username, password string) ([]byte, error) { cred := fmt.Sprintf("%s:%s", username, password) auth := base64.StdEncoding.EncodeToString([]byte(cred)) cfg := DockerConfigJSON{ Auths: map[string]DockerConfigEntry{ url: { Username: username, Password: password, Auth: auth, }, }, } return json.Marshal(cfg) } ================================================ FILE: pkg/manifestgen/sourcesecret/sourcesecret_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sourcesecret import ( "os" "reflect" "testing" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/testdata" ) func Test_passwordLoadKeyPair(t *testing.T) { tests := []struct { name string privateKeyPath string publicKeyPath string password string }{ { name: "private key pair with password", privateKeyPath: "testdata/password_rsa", publicKeyPath: "testdata/password_rsa.pub", password: "password", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pk, _ := os.ReadFile(tt.privateKeyPath) ppk, _ := os.ReadFile(tt.publicKeyPath) got, err := LoadKeyPair(pk, tt.password) if err != nil { t.Errorf("loadKeyPair() error = %v", err) return } if !reflect.DeepEqual(got.PrivateKey, pk) { t.Errorf("PrivateKey %s != %s", got.PrivateKey, pk) } if !reflect.DeepEqual(got.PublicKey, ppk) { t.Errorf("PublicKey %s != %s", got.PublicKey, ppk) } }) } } func Test_PasswordlessLoadKeyPair(t *testing.T) { for algo, privateKey := range testdata.PEMBytes { t.Run(algo, func(t *testing.T) { got, err := LoadKeyPair(privateKey, "") if err != nil { t.Errorf("loadKeyPair() error = %v", err) return } if !reflect.DeepEqual(got.PrivateKey, privateKey) { t.Errorf("PrivateKey %s != %s", got.PrivateKey, string(privateKey)) } signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { t.Errorf("unexpected error: %s", err) } if !reflect.DeepEqual(got.PublicKey, ssh.MarshalAuthorizedKey(signer.PublicKey())) { t.Errorf("PublicKey %s != %s", got.PublicKey, ssh.MarshalAuthorizedKey(signer.PublicKey())) } }) } } ================================================ FILE: pkg/manifestgen/sourcesecret/testdata/password_rsa ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB18lOcbN Q7pk768hyQUymCAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQCi/p5KtpRl tu3yxEsc+NtR9kRBuYhsh55609dNiTonLlL7K00pLCEWrtEIVM7FzD8IA6Q1NMNBFijje3 6gnqMlo6M/cJObkGfZFC025diLI5/ND5R/l60XLtRZfh92K9nCsQKsJaW+R8LTtBdhil/q M/+u07sFHzzk6/36FStg/QAAAiAQAjBJ6TYZuu4iHrrQgbhmlQem18Xjm8f2M9M0BrYgJV F5OVL7mL/bsFTp0IA92HXnxGs0gF4ue5ujCE7SWOyr4SpJfgijExnmDPkZ9nhCG06MsCQ9 uU7tTtEnrYcZ5/mmEe8E/O74Mo8xBqI8Unv95eF4p/tAMcDctVX39/lSjP1UZYL6vrLk2L SoEWK2DmZ2ZYdtBJ20AGDJ7MRIX3X/+qZkuYcy7GfPTuDKPIyrr7UEIYH//x8RaGuvOUuu P5bXGOBCTayZgGeWwNDePVITxMhpqTYjy7hqJ1ppBEv1svvbax5ksbwTRzU6quN75o+tbX hf5v0HbiRl3w6LtuwciiQtGsgijxt1noViZvpMLam5EJ3+eTnKnfEPxBMaCx7qepqPT9yI GJ/myyB/+FMkVe9epBeO2wyBTPPzr9O8co8SbcFkFEpcmxKk8toPf4F4XGs7lnibsDXGWE s2+WPmf17WLgpTMLutSVFIxw/V6ajnVrTTTE9AOwC9TYBx152YTWGILUlMMQ4AdNri8H80 k/RMv18Tut6k4v6I1aKfUSfXRaaxwagt6zMJ2TvYJFsskfTboM/FjzcDvl4jWQvfcoKJJa da6L6TwnfL03qI+tdT84R4a9oYgZ27WB0ekPnmDpWgIl5jN/F2mHz2/KpuAzC7JgpPdzty 271M16OmGvgerrvc/57d0bh4x/E9gj3ZMpkoXBH/lfs5c7LbvRxQ -----END OPENSSH PRIVATE KEY----- ================================================ FILE: pkg/manifestgen/sourcesecret/testdata/password_rsa.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCi/p5KtpRltu3yxEsc+NtR9kRBuYhsh55609dNiTonLlL7K00pLCEWrtEIVM7FzD8IA6Q1NMNBFijje36gnqMlo6M/cJObkGfZFC025diLI5/ND5R/l60XLtRZfh92K9nCsQKsJaW+R8LTtBdhil/qM/+u07sFHzzk6/36FStg/Q== ================================================ FILE: pkg/manifestgen/sourcesecret/testdata/rsa ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEA3igXbgoTAydPOiEb4VfRPM4e26S16ZXhIEt95vka8wdcru97JKFl 3yK6i0RALylrMAyjequXTNntZm2unngMuYE3OeFoY3dE1/xcWEVu8gE6AiGv74SzQsKtQv lqlXhF9s2cVyfJLmt5cvQP6zRxmx8KaZL6bN3PLDMIuq4Lit3BEn9KON89+uWKjXr9jU6/ MlaQ4jn9VYMczhMDQpTyTMFgIBLuJYa4RUQ9vpL/KvgzpXh/bSf0B7DvEt+It2Qiws+RXH sc/cdxEDNRVwBOeueqSFZjw83fg5p3G5mMKUG0MLiNk0gw3S7JZKRndKiyKQZ7UCpfiRsk YJLZGGP8BeUGC5nYLicZAYJLddHyoq8jp8AtlGcKxgAQfWTjjlI2ppJPYpM2yncgnpv8Rk QiDH9onIjTxx715IDiLmsjesFi8weQrPXQ/OkBY0qud4A/piHiSg2/mcQ4bD+yNXP7ka7n hiGI/NPPSb/Q+a9SXa72VMI3z6s4/MKXL47t1NaNAAAFiMj8bezI/G3sAAAAB3NzaC1yc2 EAAAGBAN4oF24KEwMnTzohG+FX0TzOHtuktemV4SBLfeb5GvMHXK7veyShZd8iuotEQC8p azAMo3qrl0zZ7WZtrp54DLmBNznhaGN3RNf8XFhFbvIBOgIhr++Es0LCrUL5apV4RfbNnF cnyS5reXL0D+s0cZsfCmmS+mzdzywzCLquC4rdwRJ/SjjfPfrlio16/Y1OvzJWkOI5/VWD HM4TA0KU8kzBYCAS7iWGuEVEPb6S/yr4M6V4f20n9Aew7xLfiLdkIsLPkVx7HP3HcRAzUV cATnrnqkhWY8PN34OadxuZjClBtDC4jZNIMN0uyWSkZ3SosikGe1AqX4kbJGCS2Rhj/AXl BguZ2C4nGQGCS3XR8qKvI6fALZRnCsYAEH1k445SNqaST2KTNsp3IJ6b/EZEIgx/aJyI08 ce9eSA4i5rI3rBYvMHkKz10PzpAWNKrneAP6Yh4koNv5nEOGw/sjVz+5Gu54YhiPzTz0m/ 0PmvUl2u9lTCN8+rOPzCly+O7dTWjQAAAAMBAAEAAAGBAKv82868C+YIG8UD9uKpKvrpFG i1BoR1HVn0N9+GAQAfNfjUvEAql4R9DXBeAVbBuRL05edFSpgbqzf+OA7FIAzJZajwwfEn V+vimtdXwcGng3I9BEjpMiLANoTANWzMNVYR7jRnP9ApMlf1bRGJg141VMlRGYEI46fzRp HHxnXWoe+hDiQjaIeCB5bqnbs1OL5O2FHb1S3LmJRNkduNFlyn5LRQE4CH4Mb3Qtn0UYnB p7I2LGikYr9FkoDI/74CzZkL8OK01pfgSNbVrmJ3afFra7LrMYNUqseKsWIPvwCnHjr2hL LRxW6DU6CYZjy02ZBpkmISCBFSaLNbh0rH47B842lqrFPrEdGKBvlHJLHqzKDCBW+PSaHD K30kclgO1laxx0zdTUipYPPuJLL+2iHYWMYtKdDkpS96+BjoKKen0uZUhGamk2/rCbY1Gi p/iWjNlDKExWjpnQd4KfyQvrds2KGj1+4loFLxT6akmi57aCj7rqKbiBfbaPuVUMp6HQAA AMEA8Jx79pkkCIhnA8DabHp3RBfRyaJvomka2O6XaiCdzs03U/9h8I6ROwJZ4bXXOar6n9 KrPXC4gRIBuqxoFaBcTHNIwH5apzeptJrAXe4baKJIGjG9KWP5brbyGzOsqj0Lx1bFiiro eDMOQCUFjpQ5DkwQTqpInzWLvt5bixMxQutqw4iirgB065j39UzgjPbFwI1+S/vTU2S68k qicpRmlz/hHjZX+wEPAwaUa9nBPPWv7tOqg0CYMFDwQbuT9WkuAAAAwQD28BUPrDCD4Sl+ EI5kE6eyk9tdpTlMtgiLbH2WJO9yr4C/GVmQytujb6YXHAtZ3IXf8Wg6sXY8JH36dGTqZP gJaZfzhC4SEJvCUXWim/rdxj4CQMZW7o7guMME3w5hDHitj0vyGxatp30ltwUF6/gpSiEG SAI1lSNkZk3Ey0OMZv5Tp0Y5HJ3SBlnystYzzDvlDq4m+cNCmH0IytZV8Udwjkd7knLQce gvO+vKTWf6l8nb3BlBYBV72tyKGfd9pSMAAADBAOZPMjJZtQ5yzoklmyxqUiSZl/+aB41H IHU89ejt7cynuzIvOi3HWJB4201Z0yaS19xX48httEyxk0MTb5oK1H6yYKAx7m9DVMCq2e AJRM42Hh1Eer5bh/wUqdbqrV6NWkiXP7s440ml8tAsVULCKqQPRyPo1UBkayudMBx3Ke0W 2sKWZDMT7OzC3lR4QdyC8keLzJhfudnP5ZWstOWgkTkPoZZ6EZZBz2gMVMEVczGcMYLIub eulFT3H8VUH5mIjwAAAAtoaWRkZUByaWRlcgECAwQFBg== -----END OPENSSH PRIVATE KEY----- ================================================ FILE: pkg/manifestgen/sourcesecret/testdata/rsa.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDeKBduChMDJ086IRvhV9E8zh7bpLXpleEgS33m+RrzB1yu73skoWXfIrqLREAvKWswDKN6q5dM2e1mba6eeAy5gTc54Whjd0TX/FxYRW7yAToCIa/vhLNCwq1C+WqVeEX2zZxXJ8kua3ly9A/rNHGbHwppkvps3c8sMwi6rguK3cESf0o43z365YqNev2NTr8yVpDiOf1VgxzOEwNClPJMwWAgEu4lhrhFRD2+kv8q+DOleH9tJ/QHsO8S34i3ZCLCz5Fcexz9x3EQM1FXAE5656pIVmPDzd+DmncbmYwpQbQwuI2TSDDdLslkpGd0qLIpBntQKl+JGyRgktkYY/wF5QYLmdguJxkBgkt10fKiryOnwC2UZwrGABB9ZOOOUjamkk9ikzbKdyCem/xGRCIMf2iciNPHHvXkgOIuayN6wWLzB5Cs9dD86QFjSq53gD+mIeJKDb+ZxDhsP7I1c/uRrueGIYj8089Jv9D5r1JdrvZUwjfPqzj8wpcvju3U1o0= ================================================ FILE: pkg/manifestgen/sync/options.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sync import ( "time" ) type Options struct { Interval time.Duration URL string Name string Namespace string Branch string Tag string SemVer string Commit string Secret string TargetPath string ManifestFile string RecurseSubmodules bool } func MakeDefaultOptions() Options { return Options{ Interval: 1 * time.Minute, URL: "", Name: "flux-system", Namespace: "flux-system", Branch: "main", Secret: "flux-system", ManifestFile: "gotk-sync.yaml", TargetPath: "", } } ================================================ FILE: pkg/manifestgen/sync/sync.go ================================================ /* Copyright 2020 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sync import ( "bytes" "fmt" "path" "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) func Generate(options Options) (*manifestgen.Manifest, error) { gvk := sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind) gitRef := &sourcev1.GitRepositoryRef{} if options.Branch != "" { gitRef.Branch = options.Branch } if options.Tag != "" { gitRef.Tag = options.Tag } if options.SemVer != "" { gitRef.SemVer = options.SemVer } if options.Commit != "" { gitRef.Commit = options.Commit } gitRepository := sourcev1.GitRepository{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: options.Name, Namespace: options.Namespace, }, Spec: sourcev1.GitRepositorySpec{ URL: options.URL, Interval: metav1.Duration{ Duration: options.Interval, }, Reference: gitRef, SecretRef: &meta.LocalObjectReference{ Name: options.Secret, }, RecurseSubmodules: options.RecurseSubmodules, }, } gitData, err := yaml.Marshal(gitRepository) if err != nil { return nil, err } gvk = kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind) kustomization := kustomizev1.Kustomization{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: options.Name, Namespace: options.Namespace, }, Spec: kustomizev1.KustomizationSpec{ Interval: metav1.Duration{ Duration: 10 * time.Minute, }, Path: fmt.Sprintf("./%s", strings.TrimPrefix(options.TargetPath, "./")), Prune: true, SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: sourcev1.GitRepositoryKind, Name: options.Name, }, }, } ksData, err := yaml.Marshal(kustomization) if err != nil { return nil, err } return &manifestgen.Manifest{ Path: path.Join(options.TargetPath, options.Namespace, options.ManifestFile), Content: fmt.Sprintf("%s\n---\n%s---\n%s", manifestgen.GenWarning, resourceToString(gitData), resourceToString(ksData)), }, nil } func resourceToString(data []byte) string { data = bytes.Replace(data, []byte(" creationTimestamp: null\n"), []byte(""), 1) data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) return string(data) } ================================================ FILE: pkg/manifestgen/sync/sync_test.go ================================================ //go:build !e2e // +build !e2e /* Copyright 2020 The Flux CD contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sync import ( "fmt" "strings" "testing" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestGenerate(t *testing.T) { opts := MakeDefaultOptions() output, err := Generate(opts) if err != nil { t.Fatal(err) } for _, apiVersion := range []string{sourcev1.GroupVersion.String(), kustomizev1.GroupVersion.String()} { if !strings.Contains(output.Content, apiVersion) { t.Errorf("apiVersion '%s' not found", apiVersion) } } fmt.Println(output.Content) } ================================================ FILE: pkg/manifestgen/tmpdir.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manifestgen import ( "fmt" "os" "path/filepath" ) // MkdirTempAbs creates a tmp dir and returns the absolute path to the dir. // This is required since certain OSes like MacOS create temporary files in // e.g. `/private/var`, to which `/var` is a symlink. func MkdirTempAbs(dir, pattern string) (string, error) { tmpDir, err := os.MkdirTemp(dir, pattern) if err != nil { return "", err } tmpDir, err = filepath.EvalSymlinks(tmpDir) if err != nil { return "", fmt.Errorf("error evaluating symlink: %w", err) } return tmpDir, nil } ================================================ FILE: pkg/printers/dyff.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package printers import ( "fmt" "io" "github.com/homeport/dyff/pkg/dyff" ) // DyffPrinter is a printer that prints dyff reports. type DyffPrinter struct { OmitHeader bool } // NewDyffPrinter returns a new DyffPrinter. func NewDyffPrinter() *DyffPrinter { return &DyffPrinter{ OmitHeader: true, } } // Print prints the given args to the given writer. func (p *DyffPrinter) Print(w io.Writer, args ...interface{}) error { for _, arg := range args { switch arg := arg.(type) { case dyff.Report: reportWriter := &dyff.HumanReport{ Report: arg, OmitHeader: p.OmitHeader, } if err := reportWriter.WriteReport(w); err != nil { return fmt.Errorf("failed to print report: %w", err) } default: return fmt.Errorf("unsupported type %T", arg) } } return nil } ================================================ FILE: pkg/printers/interface.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package printers import "io" // Printer is an interface for printing Flux cmd outputs. type Printer interface { // Print prints the given args to the given writer. Print(io.Writer, ...interface{}) error } // PrinterFunc is a function that can print args to a writer. type PrinterFunc func(w io.Writer, args ...interface{}) error // Print implements Printer func (fn PrinterFunc) Print(w io.Writer, args ...interface{}) error { return fn(w, args) } ================================================ FILE: pkg/printers/table_printer.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package printers import ( "fmt" "io" "github.com/olekukonko/tablewriter" ) // TablePrinter is a printer that prints Flux cmd outputs. func TablePrinter(header []string) PrinterFunc { return func(w io.Writer, args ...interface{}) error { var rows [][]string for _, arg := range args { switch arg := arg.(type) { case []interface{}: for _, v := range arg { s, ok := v.([][]string) if !ok { return fmt.Errorf("unsupported type %T", v) } rows = append(rows, s...) } default: return fmt.Errorf("unsupported type %T", arg) } } table := tablewriter.NewWriter(w) table.SetHeader(header) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") table.SetNoWhiteSpace(true) table.AppendBulk(rows) table.Render() return nil } } ================================================ FILE: pkg/status/status.go ================================================ /* Copyright 2020, 2021 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package status import ( "context" "fmt" "sort" "strings" "time" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/cli-utils/pkg/kstatus/polling" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/fluxcd/cli-utils/pkg/object" runtimeclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/flux2/v2/pkg/log" ) type StatusChecker struct { pollInterval time.Duration timeout time.Duration client client.Client statusPoller *polling.StatusPoller logger log.Logger } func NewStatusCheckerWithClient(c client.Client, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) { return &StatusChecker{ pollInterval: pollInterval, timeout: timeout, client: c, statusPoller: polling.NewStatusPoller(c, c.RESTMapper(), polling.Options{}), logger: log, }, nil } func NewStatusChecker(kubeConfig *rest.Config, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) { restMapper, err := runtimeclient.NewDynamicRESTMapper(kubeConfig) if err != nil { return nil, err } c, err := client.New(kubeConfig, client.Options{Mapper: restMapper}) if err != nil { return nil, err } return NewStatusCheckerWithClient(c, pollInterval, timeout, log) } func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error { ctx, cancel := context.WithTimeout(context.Background(), sc.timeout) defer cancel() opts := polling.PollOptions{PollInterval: sc.pollInterval} eventsChan := sc.statusPoller.Poll(ctx, identifiers, opts) coll := collector.NewResourceStatusCollector(identifiers) done := coll.ListenWithObserver(eventsChan, desiredStatusNotifierFunc(cancel, status.CurrentStatus)) <-done // we use sorted identifiers to loop over the resource statuses because a Go's map is unordered. // sorting identifiers by object's name makes sure that the logs look stable for every run sort.SliceStable(identifiers, func(i, j int) bool { return strings.Compare(identifiers[i].Name, identifiers[j].Name) < 0 }) for _, id := range identifiers { rs := coll.ResourceStatuses[id] switch rs.Status { case status.CurrentStatus: sc.logger.Successf("%s: %s ready", rs.Identifier.Name, strings.ToLower(rs.Identifier.GroupKind.Kind)) case status.NotFoundStatus: sc.logger.Failuref("%s: %s not found", rs.Identifier.Name, strings.ToLower(rs.Identifier.GroupKind.Kind)) default: sc.logger.Failuref("%s: %s not ready", rs.Identifier.Name, strings.ToLower(rs.Identifier.GroupKind.Kind)) } } if coll.Error != nil || ctx.Err() == context.DeadlineExceeded { return fmt.Errorf("timed out waiting for all resources to be ready") } return nil } // desiredStatusNotifierFunc returns an Observer function for the // ResourceStatusCollector that will cancel the context (using the cancelFunc) // when all resources have reached the desired status. func desiredStatusNotifierFunc(cancelFunc context.CancelFunc, desired status.Status) collector.ObserverFunc { return func(rsc *collector.ResourceStatusCollector, _ event.Event) { var rss []*event.ResourceStatus for _, rs := range rsc.ResourceStatuses { rss = append(rss, rs) } aggStatus := aggregator.AggregateStatus(rss, desired) if aggStatus == desired { cancelFunc() } } } ================================================ FILE: pkg/uninstall/uninstall.go ================================================ /* Copyright 2022 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package uninstall import ( "context" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" autov1 "github.com/fluxcd/image-automation-controller/api/v1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" sourcev1 "github.com/fluxcd/source-controller/api/v1" swapi "github.com/fluxcd/source-watcher/api/v2/v1beta1" "github.com/fluxcd/flux2/v2/pkg/log" "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) // Components removes all Kubernetes components that are part of Flux excluding the CRDs and namespace. func Components(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error { var aggregateErr []error opts, dryRunStr := getDeleteOptions(dryRun) selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} { var list appsv1.DeploymentList if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("Deployment/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("Deployment/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) } } } } { var list corev1.ServiceList if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("Service/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("Service/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) } } } } { var list networkingv1.NetworkPolicyList if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("NetworkPolicy/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("NetworkPolicy/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) } } } } { var list corev1.ServiceAccountList if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("ServiceAccount/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("ServiceAccount/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) } } } } { var list rbacv1.ClusterRoleList if err := kubeClient.List(ctx, &list, selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("ClusterRole/%s deletion failed: %s", r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("ClusterRole/%s deleted %s", r.Name, dryRunStr) } } } } { var list rbacv1.ClusterRoleBindingList if err := kubeClient.List(ctx, &list, selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("ClusterRoleBinding/%s deletion failed: %s", r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("ClusterRoleBinding/%s deleted %s", r.Name, dryRunStr) } } } } return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) } // Finalizers removes all finalizes on Kubernetes components that have been added by a Flux controller. func Finalizers(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error { var aggregateErr []error opts, dryRunStr := getUpdateOptions(dryRun) { var list sourcev1.GitRepositoryList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list sourcev1.OCIRepositoryList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list sourcev1.HelmRepositoryList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list sourcev1.HelmChartList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list sourcev1.BucketList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list kustomizev1.KustomizationList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list helmv2.HelmReleaseList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list notificationv1b3.AlertList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list notificationv1b3.ProviderList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list notificationv1.ReceiverList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list imagev1.ImagePolicyList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list imagev1.ImageRepositoryList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list autov1.ImageUpdateAutomationList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } { var list swapi.ArtifactGeneratorList if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { for i := range list.Items { r := list.Items[i] r.Finalizers = []string{} if err := kubeClient.Update(ctx, &r, opts); err != nil { logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) } } } } return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) } // CustomResourceDefinitions removes all Kubernetes CRDs that are a part of Flux. func CustomResourceDefinitions(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error { var aggregateErr []error opts, dryRunStr := getDeleteOptions(dryRun) selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} { var list apiextensionsv1.CustomResourceDefinitionList if err := kubeClient.List(ctx, &list, selector); err == nil { for i := range list.Items { r := list.Items[i] if err := kubeClient.Delete(ctx, &r, opts); err != nil { logger.Failuref("CustomResourceDefinition/%s deletion failed: %s", r.Name, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("CustomResourceDefinition/%s deleted %s", r.Name, dryRunStr) } } } } return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) } // Namespace removes the namespace Flux is installed in. func Namespace(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error { var aggregateErr []error opts, dryRunStr := getDeleteOptions(dryRun) ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} if err := kubeClient.Delete(ctx, &ns, opts); err != nil { logger.Failuref("Namespace/%s deletion failed: %s", namespace, err.Error()) aggregateErr = append(aggregateErr, err) } else { logger.Successf("Namespace/%s deleted %s", namespace, dryRunStr) } return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) } func getDeleteOptions(dryRun bool) (*client.DeleteOptions, string) { opts := &client.DeleteOptions{} var dryRunStr string if dryRun { client.DryRunAll.ApplyToDelete(opts) dryRunStr = "(dry run)" } return opts, dryRunStr } func getUpdateOptions(dryRun bool) (*client.UpdateOptions, string) { opts := &client.UpdateOptions{} var dryRunStr string if dryRun { client.DryRunAll.ApplyToUpdate(opts) dryRunStr = "(dry run)" } return opts, dryRunStr } ================================================ FILE: rfcs/0001-authorization/README.md ================================================ # RFC-0001 Memorandum on Flux Authorization ## Summary This RFC describes in detail, for [Flux version 0.24][] (Nov 2021), how Flux determines which operations are allowed to proceed, and how this interacts with Kubernetes' access control. ## Motivation To this point, the Flux project has provided [examples of how to make a multi-tenant system](https://github.com/fluxcd/flux2-multi-tenancy/tree/v0.1.0), but not explained exactly how they relate to Flux's authorization model; nor has the authorization model itself been documented. Further work on support for multi-tenancy, among other things, requires a full account of Flux's authorization model as a baseline. ### Goals - Give a comprehensive account of Flux's authorization model ### Non-Goals - Justify the model as it stands; this RFC simply records the state as at v0.24. ## Flux's authorization model The Flux controllers undertake operations as specified by custom resources of the kinds defined in the [Flux API][]. Most of the operations are through the Kubernetes API. Authorization for operations on external systems is not accounted for here. Flux controllers defer to [Kubernetes' native RBAC][k8s-rbac] and [namespace isolation][k8s-ns] to determine which operations are authorized, when processing the custom resources in the Flux API. In general, **Kubernetes API operations are constrained by the service account under which each controller pod runs**. In the [default deployment of Flux][flux-rbac] each controller has its own service account; and, the service accounts for the Kustomize controller and Helm controller have the [`cluster-admin` cluster role][k8s-cluster-admin] bound to it. Both the Kustomize controller and the Helm controller create, update and delete arbitrary sets of configuration that they take as user input. For example, a Kustomization object that references a GitRepository is processed by taking whatever is in the specified Git repository and applying it to the cluster. This is informally called "syncing", and these user-supplied configurations will be called "sync configurations" in the following. There are five types of access that have a distinct treatment with respect to RBAC and namespace isolation: - reading and writing the Flux API object to be processed - accessing dependencies of a Flux API object; for example, a secret that holds a decryption key - accessing Flux API objects related to the object being processed; for example, a GitRepository referenced by a Kustomization - creating, updating and deleting Flux API objects as part of processing; for example, each `HelmRelease` object contains a template for a Helm chart spec, which the Helm controller uses to create a `HelmChart` object - creating, updating, deleting, and health-checking of arbitrary objects as specified by _sync configurations_ (as mentioned above). This table summarises how these operations are subject to RBAC and namespace isolation. | Type of operation | Accessed via | Namespace isolation | |------------------------------------------------|----------------------------|------------------------------| | Reading and writing the object to be processed | Controller service account | N/A | | Dependencies of object to be processed | Controller service account | Same namespace only | | Access to related Flux API objects | Controller service account | Some cross-namespace refs[1] | | CRUD of Flux API objects | Controller service account | Created in same namespace | | CRUD and healthcheck of sync configurations | Impersonation[2] | As directed by spec[2] | [1] See "Cross-namespace references" below
[2] See "Impersonation" below There are two related mechanisms that affect the service account used for the operations marked with "Impersonation" above: "impersonation" and "remote apply". These are explained in the following sections. ### Impersonation The Kustomize controller and Helm controller both apply arbitrary sets of Kubernetes configuration ("_synced configuration_" as above) to a cluster. These controllers use the service account named in the field `.spec.serviceAccountName` in the `Kustomization` and `HelmRelease` objects respectively, while applying and health-checking the synced configuration. This mechanism is called "impersonation". The `.spec.serviceAccountName` field is optional. If empty, the controller's service account is used. ### Remote apply The Kustomize controller and Helm controller are able to apply a set of configuration to a cluster other than the cluster in which they run. If the `Kustomization` or `HelmRelease` object [refers to a secret containing a "kubeconfig" file][kubeconfig], the controller will construct a client using that kubeconfig, and the client is used to apply the prepared set of configuration. The effect of this is that the configuration will be applied as the user given in the kubeconfig; often this is a user with the `cluster-admin` role bound to it, but not necessarily so. All accesses that would use impersonation use the remote client instead. ### Cross-namespace references Some Flux API kinds have fields which can refer to a Flux API object in another namespace. The Flux controllers do not respect namespace isolation when dereferencing these fields. The following are fields that are not restricted to the namespace of the containing object, listed by API kind. | API kind | field | explanation | |----------|-------|-------------| | **`kustomizations.kustomize.toolkit.fluxcd.io/v1beta2`** | `.spec.dependsOn` | Items are references that can include a namespace | | | `.spec.healthChecks` | Items are references that can include a namespace (note: these are accessed using impersonation) | | | `.spec.sourceRef` | This is a reference that can include a namespace | | | `.spec.targetNamespace` | This sets or overrides the namespace given in the top-most `kustomization.yaml` | | **`helmreleases.helm.toolkit.fluxcd/v2beta1`** | `.spec.dependsOn` | Items are references that can include a namespace | | | `.spec.targetNamespace` | This gives the namespace into which a Helm chart is installed (note: using impersonation) | | | `.spec.storageNamespace` | This gives the namespace in which the record of a Helm install is created (note: using impersonation) | | | `.spec.chart.spec.sourceRef` | This is a reference (in the created `HelmChart` object) that can include a namespace | | **`alerts.notification.toolkit.fluxcd.io/v1beta2`** | `.spec.eventSources` | Items are references that can include a namespace | | **`receivers.notification.toolkit.fluxcd.io/v1beta2`** | `.spec.resources` | Items in this field are references that can include a namespace | | **`imagepolicies.image.toolkit.fluxcd.io/v1beta1`** | `.spec.imageRepositoryRef` | This reference can include a namespace[1] | [1] This particular cross-namespace reference is subject to additional access control; see "Access control for cross-namespace references" below. Note that the field `.spec.sourceRef` of **`imageupdateautomation.image.toolkit.fluxcd.io`** does _not_ include a namespace. #### Access control for cross-namespace references In v0.24, an `ImagePolicy` object can refer to a `ImageRepository` object in another namespace. Unlike most cross-namespace references, the controller processing `ImagePolicy` objects applies additional access control, as given in the referenced `ImageRepository`: the field [`.spec.accessFrom`][access-from-ref] grants access to the namespaces selected therein. Access is denied unless granted. ## Security considerations ### Impersonation is optional Flux does not insist on a service account to be supplied in `Kustomization` and `HelmRelease` specifications, and the default is to use the controller's service account. That means a user with the ability to create either of those objects can trivially arrange for a configuration to be applied with the controller service account, which in the default deployment of Flux will have `cluster-admin` bound to it. This represents a privilege escalation vulnerability in the default deployment of Flux. To guard against it, an admission controller can be used to make the `.spec.serviceAccountName` field mandatory; an example which uses Kyverno is given in [the multi-tenancy implementation][multi-tenancy-eg]. ### Cross-namespace references side-step namespace isolation `HelmRelease` and `Kustomization` objects can refer to `GitRepository`, `HelmRepository`, or `Bucket` (collectively "sources") in any other namespace. The referenced objects are accessed through the controller's service account, which by default has `cluster-admin` bound to it. This means all sources in a cluster are by default usable as a synced configuration, from any namespace. To restrict access, an admission controller can be used to block cross-namespace references; the [example using Kyverno][multi-tenancy-eg] from above also does this. ## References - [CVE-2021-41254](https://github.com/fluxcd/kustomize-controller/security/advisories/GHSA-35rf-v2jv-gfg7) "Privilege escalation to cluster admin on multi-tenant environments" was fixed in flux2 **v0.15.0**. [Flux version 0.24]: https://github.com/fluxcd/flux2/releases/tag/v0.24.0 [serviceAccountName]: https://fluxcd.io/docs/components/kustomize/api/#kustomize.toolkit.fluxcd.io/v1beta2.KustomizationSpec [kubeconfig]: https://fluxcd.io/docs/components/kustomize/api/#kustomize.toolkit.fluxcd.io/v1beta2.KubeConfig [access-from-ref]: https://fluxcd.io/docs/components/image/imagerepositories/#allow-cross-namespace-references [Flux API]: https://fluxcd.io/docs/components/ [flux-rbac]: https://github.com/fluxcd/flux2/tree/v0.24.0/manifests/rbac [k8s-ns]: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ [k8s-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ [k8s-cluster-admin]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles [multi-tenancy-eg]: https://github.com/fluxcd/flux2-multi-tenancy/blob/main/infrastructure/kyverno-policies/flux-multi-tenancy.yaml ================================================ FILE: rfcs/0002-helm-oci/README.md ================================================ # RFC-0002 Flux OCI support for Helm **Status:** implemented (partially) **Creation date:** 2022-03-30 **Last update:** 2023-11-28 ## Summary Given that Helm v3.8 supports [OCI](https://helm.sh/docs/topics/registries/) for package distribution, we should extend the Flux Source API to allow fetching Helm charts from container registries. ## Motivation Helm OCI support is one of the most requested feature in Flux as seen on this [issue](https://github.com/fluxcd/source-controller/issues/124). With OCI support, Flux users can automate chart updates to Git in the same way they do today for container images. ### Goals - Add support for fetching Helm charts stored as OCI artifacts with minimal API changes to Flux. - Add support for verifying the authenticity of Helm OCI charts signed with Cosign. - Make it easy for users to switch from [HTTP/S Helm repositories](https://github.com/helm/helm-www/blob/416fabea6ffab8dc156b6a0c5eb5e8df5f5ef7dc/content/en/docs/topics/chart_repository.md) to OCI repositories. ### Non-Goals - Introduce a new API kind for referencing charts stored as OCI artifacts. ## Proposal Introduce an optional field called `type` to the `HelmRepository` spec. When not specified, the `spec.type` field defaults to `default` which preserve the current `HelmRepository` API behaviour. When the `spec.type` field is set to `oci`, the `spec.url` field must be prefixed with `oci://` (to follow the Helm conventions). For `oci://` URLs, source-controller will use the Helm SDK and the `oras` library to connect to the OCI remote storage. Introduce an optional field called `provider` for [context-based authorization](https://fluxcd.io/flux/security/contextual-authorization/) to AWS, Azure and Google Cloud. The `spec.provider` is ignored when `spec.type` is set to `default`. ### Pull charts from private repositories #### Basic auth For private repositories hosted on GitHub, Quay, self-hosted Docker Registry and others, the credentials can be supplied with: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: spec: type: oci secretRef: name: regcred ``` The `secretRef` points to a Kubernetes secret in the same namespace as the `HelmRepository`. The [secret type](https://kubernetes.io/docs/concepts/configuration/secret/#secret-types) must be `kubernetes.io/dockerconfigjson`: ```shell kubectl create secret docker-registry regcred \ --docker-server= \ --docker-username= \ --docker-password= ``` #### OIDC auth When Flux runs on AKS, EKS or GKE, an IAM role (that grants read-only access to ACR, ECR or GCR) can be used to bind the `source-controller` to the IAM role. ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: spec: type: oci provider: azure ``` The provider accepts the following values: `generic`, `aws`, `azure` and `gcp`. When the provider is not specified, it defaults to `generic`. When the provider is set to `aws`, `azure` or `gcp`, the controller will use a specific cloud SDK for authentication purposes. If both `spec.secretRef` and a non-generic provider are present in the definition, the controller will use the static credentials from the referenced secret. ### Verify Helm charts To verify the authenticity of the Helm OCI charts, Flux will use the Sigstore Go SDK and implement verification for artifacts which were either signed with keys generated by Cosign or signed using the Cosign [keyless method](https://github.com/sigstore/cosign/blob/main/KEYLESS.md). To enable signature verification, the Cosign public keys can be supplied with: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmChart metadata: name: spec: verify: provider: cosign secretRef: name: cosign-public-keys ``` Note that the Kubernetes secret containing the Cosign public keys, must use `.pub` extension: ```yaml apiVersion: v1 kind: Secret metadata: name: cosign-public-keys type: Opaque stringData: key1.pub: key2.pub: ``` For verifying public Helm charts which are signed using the keyless method, the `spec.verify.secretRef` field must be omitted: ```yaml spec: verify: provider: cosign ``` When using the keyless method, Flux will verify the signatures in the Rekor transparency log instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). ### User Stories #### Story 1 > As a developer I want to use Flux `HelmReleases` that refer to Helm charts stored > as OCI artifacts in GitHub Container Registry. First create a secret using a GitHub token that allows access to GHCR: ```sh kubectl create secret docker-registry ghcr-charts \ --docker-server=ghcr.io \ --docker-username=$GITHUB_USER \ --docker-password=$GITHUB_TOKEN ``` Then define a `HelmRepository` of type `oci` and reference the `dockerconfig` secret: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: ghcr-charts namespace: default spec: type: oci url: oci://ghcr.io/my-org/charts/ secretRef: name: ghcr-charts ``` And finally in Flux `HelmReleases`, refer to the ghcr-charts `HelmRepository`: ```yaml apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: name: my-app namespace: default spec: interval: 60m chart: spec: chart: my-app version: '1.0.x' sourceRef: kind: HelmRepository name: ghcr-charts interval: 1m # check for new OCI artifacts every minute ``` #### Story 2 > As a platform admin I want to automate Helm chart updates based on a semver ranges. > When a new patch version is available in the container registry, I want Flux to open a PR > with the version set in the `HelmRelease` manifests. Given that charts are stored in container registries, you can use Flux image automation and patch the chart version in Git, in the same way Flux works for updating container image tags. Define an image registry and a policy for the chart artifact: ```yaml apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository metadata: name: my-app namespace: default spec: image: ghcr.io/my-org/charts/my-app interval: 1m0s --- apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImagePolicy metadata: name: my-app namespace: default spec: imageRepositoryRef: name: my-app policy: semver: range: 1.0.x ``` Then add the policy marker to the `HelmRelease` manifests in Git: ```yaml apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: name: my-app namespace: default spec: interval: 60m chart: spec: chart: my-app version: 1.0.0 # {"$imagepolicy": "default:my-app:tag"} sourceRef: kind: HelmRepository name: ghcr-charts interval: 1m ``` ### Alternatives We could introduce a new API type e.g. `HelmRegistry` to hold the reference to auth secret, as proposed in [#2573](https://github.com/fluxcd/flux2/pull/2573). That is considered unpractical, as there is no benefit for users in having a dedicated kind instead of a `type` field in the current `HelmRepository` API. Adding a `type` field to the spec follows the Flux Bucket API design, where the same Kind servers different implementations: AWS S3 vs Azure Blob vs Google Storage. ## Design Details Unlike the default `HelmRepository`, the OCI `HelmRepository` does not need to download any repository index file. The associated HelmChart can pull the chart directly from the OCI registry based on the registry information in the `HelmRepository` object. This makes the `HelmRepository` of type `oci` static, not backed by a reconciler to move to a desired state. It becomes a data container with information about the OCI registry. In source-controller, the `HelmRepositoryReconciler` will be updated to check the `.spec.type` field of `HelmRepository` and do nothing if it is `oci`. The current `HelmChartReconciler` will be adapted to handle both types. ### Enabling the feature The feature is enabled by default. ## Implementation History * **2022-05-19** Partially implemented by [source-controller#690](https://github.com/fluxcd/source-controller/pull/690) * **2022-06-06** First implementation released with [flux2 v0.31.0](https://github.com/fluxcd/flux2/releases/tag/v0.31.0) * **2022-08-11** Resolve chart dependencies from OCI released with [flux2 v0.32.0](https://github.com/fluxcd/flux2/releases/tag/v0.32.0) * **2022-08-29** Contextual login for AWS, Azure and GCP released with [flux2 v0.33.0](https://github.com/fluxcd/flux2/releases/tag/v0.33.0) * **2022-10-21** Verifying Helm charts with Cosign released with [flux2 v0.36.0](https://github.com/fluxcd/flux2/releases/tag/v0.36.0) * **2023-11-28** Update the design of HelmRepository of type OCI to be static object [flux2 v2.2.0](https://github.com/fluxcd/flux2/releases/tag/v2.2.0) ### TODOs * [Add support for container registries with self-signed TLS certs](https://github.com/fluxcd/source-controller/issues/723) ================================================ FILE: rfcs/0003-kubernetes-oci/README.md ================================================ # RFC-0003 Flux OCI support for Kubernetes manifests **Status:** implemented **Creation date:** 2022-03-31 **Last update:** 2023-11-07 ## Summary Flux should be able to distribute and reconcile Kubernetes configuration packaged as OCI artifacts. On the client-side, the Flux CLI should offer a command for packaging Kubernetes configs into an OCI artifact and pushing the artifact to a container registry using the Docker config file and the Docker credential helpers for authentication. On the server-side, the Flux source-controller should offer a dedicated API Kind for defining how OCI artifacts are pulled from container registries and how the artifact's authenticity can be verified. Flux should be able to work with any type of artifact even if it's not created with the Flux CLI. ## Motivation Given that OCI registries are evolving into a generic artifact storage solution, we should extend Flux to allow fetching Kubernetes manifests and related configs from container registries similar to how Flux works with Git and Bucket storage. With OCI support, Flux users can automate artifact updates to Git in the same way they do today for container images. ### Goals - Add support to the Flux CLI for packaging Kubernetes manifests and related configs into OCI artifacts. - Add support to Flux source-controller for fetching configs stored as OCI artifacts. - Make it easy for users to switch from Git repositories and Buckets to OCI repositories. ### Non-Goals - Enforce a specific OCI media type for artifacts containing Kubernetes manifests or any other configs. ## Proposal ### Push artifacts Flux users should be able to package a local directory containing Kubernetes configs into a tarball and push the archive to a container registry as an OCI artifact. ```sh flux push artifact oci://docker.io/org/app-config:v1.0.0 \ --source="$(git config --get remote.origin.url)" \ --revision="sha1:$(git rev-parse HEAD)" \ --path="./deploy" ``` The Flux CLI will produce OCI artifacts by setting the config layer media type to `application/vnd.cncf.flux.config.v1+json`. The directory pointed to by `--path` is archived and compressed in the `tar+gzip` format and the layer media type is set to `application/vnd.cncf.flux.content.v1.tar+gzip`. The source and revision are added to the OCI artifact as Open Containers standard annotations: ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "annotations": { "org.opencontainers.image.created": "2023-02-10T09:06:09Z", "org.opencontainers.image.revision": "sha1:6ea3e5b4da159fcb4a1288f072d34c3315644bcc", "org.opencontainers.image.source": "https://github.com/fluxcd/flux2" } } ``` To ease the promotion workflow of a specific version from one environment to another, the CLI should offer a tagging command. ```sh flux tag artifact oci://docker.io/org/app-config:v1.0.0 --tag=latest --tag=production ``` To view all the available artifacts in a repository and their metadata, the CLI should offer a list command. ```sh flux list artifacts oci://docker.io/org/app-config ``` To help inspect artifacts, the Flux CLI will offer a `build` and a `pull` command for generating tarballs locally and for downloading the tarballs from remote container registries. ```sh flux build artifact --path ./deploy --output tmp/artifact.tgz flux pull artifact oci://docker.io/org/app-config:v1.0.0 --output ./manifests ``` ### Pull artifacts Flux users should be able to define a source for pulling manifests inside the cluster from an OCI repository. ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: name: app-config namespace: flux-system spec: interval: 10m url: oci://docker.io/org/app-config ref: tag: v1.0.0 ``` The `spec.url` field points to the container image repository in the format `oci://://`. Note that specifying a tag or digest is not in accepted for this field. The `spec.url` value is used by the controller to fetch the list of tags from the remote OCI repository. An `OCIRepository` can refer to an artifact by tag, digest or semver range: ```yaml spec: ref: # one of tag: "latest" digest: "sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2" semver: "6.0.x" ``` ### Layer selection By default, Flux assumes that the first layer of the OCI artifact contains the Kubernetes configuration. For multi-layer artifacts created by other tools than Flux CLI (e.g. [oras](https://github.com/oras-project/oras), [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane)), users can specify the [media type](https://github.com/opencontainers/image-spec/blob/v1.0.2/media-types.md) of the layer which contains the tarball with Kubernetes manifests. ```yaml spec: layerSelector: mediaType: "application/vnd.cncf.flux.content.v1.tar+gzip" ``` If the layer selector matches more than one layer, the first layer matching the specified media type will be used. Note that Flux requires that the OCI layer is [compressed](https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types) in the `tar+gzip` format. ### Pull artifacts from private repositories For authentication purposes, Flux users can choose between supplying static credentials with Kubernetes secrets and cloud-based OIDC using an IAM role binding to the source-controller Kubernetes service account. #### Basic auth For private repositories hosted on DockerHub, GitHub, Quay, self-hosted Docker Registry and others, the credentials can be supplied with: ```yaml spec: secretRef: name: regcred ``` The `secretRef` points to a Kubernetes secret in the same namespace as the `OCIRepository`, the secret type must be `kubernetes.io/dockerconfigjson`: ```shell kubectl create secret docker-registry regcred \ --docker-server= \ --docker-username= \ --docker-password= ``` For image pull secrets attached to a service account, the account name can be specified with: ```yaml spec: serviceAccountName: regsa ``` #### Client cert auth For private repositories which require a certificate to authenticate, the client certificate, private key and the CA certificate (if self-signed), can be provided with: ```yaml spec: certSecretRef: name: regcert ``` The `certSecretRef` points to a Kubernetes secret in the same namespace as the `OCIRepository`: ```shell kubectl create secret generic regcert \ --from-file=certFile=client.crt \ --from-file=keyFile=client.key \ --from-file=caFile=ca.crt ``` #### OIDC auth When Flux runs on AKS, EKS or GKE, an IAM role (that grants read-only access to ACR, ECR or GCR) can be used to bind the `source-controller` to the IAM role. ```yaml spec: provider: aws ``` The provider accepts the following values: `generic`, `aws`, `azure` and `gcp`. When the provider is not specified, it defaults to `generic`. When the provider is set to `aws`, `azure` or `gcp`, the controller will use a specific cloud SDK for authentication purposes. If both `spec.secretRef` and a non-generic provider are present in the definition, the controller will use the static credentials from the referenced secret. ### Verify artifacts To verify the authenticity of the OCI artifacts, Flux will use the Sigstore Go SDK and implement verification for artifacts which were either signed with keys generated by Cosign or signed using the Cosign [keyless method](https://github.com/sigstore/cosign/blob/main/KEYLESS.md). To enable signature verification, the Cosign public key can be supplied with: ```yaml spec: verify: provider: cosign secretRef: name: cosign-key ``` For verifying public artifacts which are signed using the keyless method, the `.spec.verify.matchOIDCIdentity` field must be used instead of `spec.verify.secretRef`. ```yaml spec: verify: provider: cosign matchOIDCIdentity: - issuer: "^https://token.actions.githubusercontent.com$" subject: "^https://github.com/org/app-repository.*$" ``` The `matchOIDCIdentity` entries must contain the following fields: - `.issuer`, regexp that matches against the OIDC issuer. - `.subject`, regexp that matches against the subject identity in the certificate. The entries are evaluated in an OR fashion, i.e. the identity is deemed to be verified if any one entry successfully matches against the identity. When using the keyless method, Flux will verify the signatures in the Rekor transparency log instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). ### Reconcile artifacts The `OCIRepository` can be used as a drop-in replacement for `GitRepository` and `Bucket` sources. For example, a Flux Kustomization can refer to an `OCIRepository` and reconcile the manifests found in the OCI artifact: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization metadata: name: app namespace: flux-system spec: interval: 10m sourceRef: kind: OCIRepository name: app-config path: ./ ``` ### User Stories #### Story 1 > As a developer I want to publish my app Kubernetes manifests to the same GHCR registry > where I publish my app containers. First login to GHCR with Docker: ```sh docker login ghcr.io -u ${GITHUB_USER} -p ${GITHUB_TOKEN} ``` Build your app container image and push it to GHCR: ```sh docker build -t ghcr.io/org/my-app:v1.0.0 . docker push ghcr.io/org/my-app:v1.0.0 ``` Edit the app deployment manifest and set the new image tag. Then push the Kubernetes manifests to GHCR: ```sh flux push artifact oci://ghcr.io/org/my-app-config:v1.0.0 \ --source="$(git config --get remote.origin.url)" \ --revision="sha1:$(git rev-parse HEAD)"\ --path="./deploy" ``` Sign the config image with cosign: ```sh cosign sign --key cosign.key ghcr.io/org/my-app-config:v1.0.0 ``` Mark `v1.0.0` as latest: ```sh flux tag artifact oci://ghcr.io/org/my-app-config:v1.0.0 --tag latest ``` List the artifacts and their metadata with: ```console $ flux list artifacts oci://ghcr.io/org/my-app-config ARTIFACT DIGEST SOURCE REVISION ghcr.io/org/my-app-config:latest sha256:45b95019d30af335137977a369ad56e9ea9e9c75bb01afb081a629ba789b890c https://github.com/org/my-app-config.git sha1:20b3a674391df53f05e59a33554973d1cbd4d549 ghcr.io/org/my-app-config:v1.0.0 sha256:45b95019d30af335137977a369ad56e9ea9e9c75bb01afb081a629ba789b890c https://github.com/org/my-app-config.git sha1:3f45e72f0d3457e91e3c530c346d86969f9f4034 ``` #### Story 2 > As a developer I want to deploy my app using Kubernetes manifests published as OCI artifacts to GHCR. First create a secret using a GitHub token that allows access to GHCR: ```sh kubectl create secret docker-registry my-app-regcred \ --docker-server=ghcr.io \ --docker-username=$GITHUB_USER \ --docker-password=$GITHUB_TOKEN ``` Then create a secret with your cosgin public key: ```sh kubectl create secret generic my-app-cosgin-key \ --from-file=cosign.pub=cosign/my-key.pub ``` Then define an `OCIRepository` to fetch and verify the latest app config version: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: name: app-config namespace: default spec: interval: 10m url: oci://ghcr.io/org/my-app-config ref: semver: "1.x" secretRef: name: my-app-regcred verify: provider: cosign secretRef: name: my-app-cosgin-key ``` And finally, create a Flux Kustomization to reconcile the app on the cluster: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization metadata: name: app namespace: default spec: interval: 10m sourceRef: kind: OCIRepository name: app-config path: ./deploy prune: true wait: true timeout: 2m ``` ## Design Details The Flux controllers and CLI will use the [fluxcd/pkg/oci](https://github.com/fluxcd/pkg/tree/main/oci) library for OCI operations such as push, pull, tag, list tags, etc. For authentication purposes, the `flux artifact` commands will use the `~/.docker/config.json` config file and the Docker credential helpers. On Cloud VMs without Docker installed, the CLI will use context-based authorization for AWS, Azure and GCP. The Flux CLI will produce OCI artifacts with the following format: ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": { "mediaType": "application/vnd.cncf.flux.config.v1+json", "size": 233, "digest": "sha256:1b80ecb1c04d4a9718a6094a00ed17b76ea8ff2bb846695fa38e7492d34f505c" }, "layers": [ { "mediaType": "application/vnd.cncf.flux.content.v1.tar+gzip", "size": 19081, "digest": "sha256:46c2b334705cd08db1a6fa46f860cd3364fc1a3636eea37a9b35537549086a1c" } ], "annotations": { "org.opencontainers.image.created": "2023-02-10T09:06:09Z", "org.opencontainers.image.revision": "sha1:6ea3e5b4da159fcb4a1288f072d34c3315644bcc", "org.opencontainers.image.source": "https://github.com/fluxcd/flux2" } } ``` The source-controller will extract the first layer from the OCI artifact, and will repackage it as an internal `sourcev1.Artifact`. The internal artifact revision will be set to the OCI SHA256 digest and the OpenContainers annotation will be copied to the internal artifact metadata: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: creationTimestamp: "2022-06-22T09:14:19Z" finalizers: - finalizers.fluxcd.io generation: 1 name: podinfo namespace: oci resourceVersion: "6603" uid: 42e0b9f0-021c-476d-86c7-2cd20747bfff spec: interval: 10m ref: tag: 6.1.6 timeout: 60s url: oci://ghcr.io/stefanprodan/manifests/podinfo status: artifact: checksum: d7e924b4882e55b97627355c7b3d2e711e9b54303afa2f50c25377f4df66a83b lastUpdateTime: "2022-06-22T09:14:21Z" metadata: org.opencontainers.image.created: "2023-02-10T09:06:09Z" org.opencontainers.image.revision: sha1:b3b00fe35424a45d373bf4c7214178bc36fd7872 org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git path: ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz revision: sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de size: 1105 url: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz conditions: - lastTransitionTime: "2022-06-22T09:14:21Z" message: stored artifact for revision 'sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' observedGeneration: 1 reason: Succeeded status: "True" type: Ready - lastTransitionTime: "2022-06-22T09:14:21Z" message: stored artifact for revision 'sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' observedGeneration: 1 reason: Succeeded status: "True" type: ArtifactInStorage observedGeneration: 1 url: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/latest.tar.gz ``` ### Enabling the feature The feature is enabled by default. ## Implementation History * **2022-08-08** Partially implemented by [source-controller#788](https://github.com/fluxcd/source-controller/pull/788) * **2022-08-11** First implementation released with [flux2 v0.32.0](https://github.com/fluxcd/flux2/releases/tag/v0.32.0) * **2022-08-29** Select layer by OCI media type released with [flux2 v0.33.0](https://github.com/fluxcd/flux2/releases/tag/v0.33.0) * **2022-09-29** Verifying OCI artifacts with Cosign released with [flux2 v0.35.0](https://github.com/fluxcd/flux2/releases/tag/v0.35.0) * **2023-02-20** Custom OCI media types released with [flux2 v0.40.0](https://github.com/fluxcd/flux2/releases/tag/v0.40.0) * **2023-10-31** OIDC identity verification implemented in [source-controller#1250](https://github.com/fluxcd/source-controller/pull/1250) ================================================ FILE: rfcs/0004-insecure-http/README.md ================================================ # RFC-0004 Block insecure HTTP connections across Flux **Status:** implementable **Creation Date:** 2022-09-08 **Last update:** 2023-07-26 ## Summary Flux should have a consistent way of disabling insecure HTTP connections. At the controller level, a flag should be present which would disable all outgoing HTTP connections. At the object level, a field should be provided which would enable the use of non-TLS endpoints. If the use of a non-TLS endpoint is not supported, reconciliation will fail and the object will be marked as stalled, signalling that human intervention is required. ## Motivation Today the use of non-TLS based connections is inconsistent across Flux controllers. Controllers that deal only with `http` and `https` schemes have no way to block use of the `http` scheme at controller-level. Some Flux objects provide a `.spec.insecure` field to enable the use of non-TLS based endpoints, but they don't clearly notify users when the option is not supported (e.g. Azure/GCP Buckets). ### Goals * Provide a flag across relevant Flux controllers which disables all outgoing HTTP connections. * Add a field which enables the use of non-TLS endpoints to appropriate Flux objects. * Provide a way for users to be made aware that their use of non-TLS endpoints is not supported if that is the case. ### Non-Goals * Break Flux's current behavior of allowing HTTP connections. * Change in behavior of communication between Flux components. ## Proposal ### Controllers Flux users should be able to enforce that controllers are using HTTPS connections only. This shall be enabled by adding a new boolean flag `--insecure-allow-http` to the following controllers: * source-controller * notification-controller * image-automation-controller * image-reflector-controller The default value of this flag shall be `true`. This would ensure that there is no breaking change with controllers still being able to access non-TLS endpoints. To disable this behavior and enforce the use of HTTPS connections, users would have to explicitly pass the flag to the controller: ```yaml spec: template: spec: containers: - name: manager image: fluxcd/source-controller args: - --watch-all-namespaces - --log-level=info - --log-encoding=json - --enable-leader-election - --storage-path=/data - --storage-adv-addr=source-controller.$(RUNTIME_NAMESPACE).svc.cluster.local. - --insecure-allow-http=false ``` **Note:** The flag shall not be added to the following controllers: * kustomize-controller: This flag is excluded from this controller, as the upstream `kubenetes-sigs/kustomize` project does not support disabling HTTP connections while fetching resources from remote bases. We can revisit this if the upstream project adds support for this at a later point in time. * helm-controller: This flag does not serve a purpose in this controller, as the controller does not make any HTTP calls. Furthermore although both controllers can also do remote applies, serving `kube-apiserver` over plain HTTP is disabled by default. While technically this can be enabled, the option for this configuration was also disabled quite a while back (ref: https://github.com/kubernetes/kubernetes/pull/65830/). ### Objects Some Flux objects, like `GitRepository`, provide a field for specifying a URL, and the URL would contain the scheme. In such cases, the scheme can be used for inferring the transport type of the connection and consequently, whether to use HTTP or HTTPS connections for that object. But there are a few objects that don't allow such behavior, for example: * `ImageRepository`: It provides a field, `.spec.image`, which is used for specifying the address of the image present on a container registry. But any address containing a scheme is considered invalid and HTTPS is the default transport used. This prevents users from using images present on insecure registries. * OCI `HelmRepository`: When using an OCI registry as a Helm repository, the `.spec.url` is expected to begin with `oci://`. Since the scheme part of the URL is used to specify the type of `HelmRepository`, there is no way for users to specify that the registry is hosted at a non-TLS endpoint. For such objects, we shall introduce a new boolean field `.spec.insecure`, which shall be `false` by default. Users that need their object to point to an HTTP endpoint, can set this to `true`. ### CLI The Flux CLI offers several commands for creating Flux specific resources. Some of these commands may involve specifying an endpoint such as creating an `OCIRepository`: ```sh flux create source oci podinfo \ --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag=6.1.6 \ --interval=10m ``` Since these commands essentially create object definitions, the CLI should offer a boolean flag `--insecure` for the required commands, which will be used for specifying the value of `.spec.insecure` of such objects. > Note: This flag should not be confused with `--insecure-skip-tls-verify` which is meant to skip TLS verification > when using an HTTPS connection. ### Proxy The flag shall also apply to all possible proxy configurations. If the flag `--insecure-allow-http` is set to `false`, then specifying the `HTTP_PROXY` environment variable to the controller will lead to the controller exiting with a failure on startup. This also applies for when the `HTTPS_PROXY` enviornment variable's value is a URL that has `http` as its scheme. Similarly, if a proxy is specified using the object's API, such as through `.spec.secretRef` in `Provider` in the `notification.toolkit.fluxcd.io` API group and the proxy URL has `http` as its scheme, the reconciler will fail and return an error, which can be viewed in the controller logs and the object's events. ### Precedence & Validity Objects with `.spec.insecure` as `true` will only be allowed if HTTP connections are allowed at the controller level. Similarly, an object can have `.spec.insecure` as `true` only if the Saas/Cloud provider allows HTTP connections. For example, using a `Bucket` with its `.spec.provider` set to `azure` would be invalid since Azure doesn't allow HTTP connections. ### User Stories #### Story 1 > As a cluster admin of a multi-tenant cluster, I want to ensure all controllers access endpoints using only HTTPS > regardless of tenants' object definitions. Apply a `kustomize` patch which prevents the use of HTTP connections: ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - gotk-components.yaml - gotk-sync.yaml patches: - patch: | - op: add path: /spec/template/spec/containers/0/args/- value: --insecure-allow-http=false target: kind: Deployment name: "(source-controller|notification-controller|image-reflector-controller|image-automation-controller)" # Since the above flag is not available in kustomize-controller for reasons explained in a previous section, # we disable Kustomize remote builds by disallowing use of remote bases. This ensures that kustomize-controller # won't initiate any plain HTTP connections. - patch: | - op: add path: /spec/template/spec/containers/0/args/- value: --no-remote-bases=true target: kind: Deployment name: kustomize-controller ``` #### Story 2 > As an application developer, I'm trying to debug a new image pushed to my local registry which > is not served over HTTPS. Modify the object spec to use HTTP connections explicitly: ```yaml apiVersion: image.toolkit.fluxcd.io/v1beta1 kind: ImageRepository metadata: name: podinfo namespace: flux-system spec: image: kind-registry:5000/stefanprodan/podinfo interval: 1m0s insecure: true ``` ### Alternatives Instead of adding a flag, we can instruct users to make use of Kyverno policies to enforce that all objects have `.spec.insecure` as `false` and any URLs present in the definition don't have `http` as the scheme. This is less attractive, as this would ask users to install another software and prevent Flux multi-tenancy from being standalone. ## Design Details If a controller is started with `--insecure-allow-http=false`, any URL in a Flux object which has `http` as the scheme will result in an unsuccessful reconciliation and the following condition will be added to the object's `.status.conditions`: ```yaml status: conditions: - lastTransitionTime: "2022-09-06T09:14:21Z" message: "Use of insecure HTTP connections isn't allowed for this controller" observedGeneration: 1 reason: InsecureConnectionsDisallowed status: "True" type: Stalled ``` Similarly, if an object has `.spec.insecure` as `true` but the Cloud provider doesn't allow HTTP connections, the reconciliation will fail and the following condition will be added to the object's `.status.conditions`: ```yaml status: conditions: - lastTransitionTime: "2022-09-06T09:14:21Z" message: "Use of insecure HTTP connections isn't allowed for Azure Storage" observedGeneration: 1 reason: UnsupportedConnectionType status: "True" type: Stalled ``` If an object has `.spec.insecure` as `true`, the registry client or bucket client shall be created with the use of HTTP connections enabled explicitly. ## Implementation History **2022-08-12** Allow defining OCI sources for non-TLS container registries with `flux create source oci --insecure` released with [flux2 v0.34.0](https://github.com/fluxcd/flux2/releases/tag/v0.34.0) ================================================ FILE: rfcs/0005-artifact-revision-and-digest/README.md ================================================ # RFC-0005 Artifact `Revision` format and introduction of `Digest` **Status:** implemented **Creation date:** 2022-10-20 **Last update:** 2023-02-20 ## Summary This RFC proposes to establish a canonical `Revision` format for an `Artifact` which points to a specific revision represented as a checksum (e.g. an OCI manifest digest or Git commit SHA) of a named pointer (e.g. an OCI repository name or Git tag). In addition, it proposes to include the algorithm name (e.g. `sha256`) as a prefix to an advertised checksum for an `Artifact` and further referring to it as a `Digest`, deprecating the `Checksum` field. ## Motivation The current `Artifact` type's `Revision` field format is not "officially" standardized (albeit assumed throughout our code bases), and has mostly been derived from `GitRepository` which uses `/` as a separator between the named pointer (a Git branch or tag) and a specific (SHA-1, or theoretical SHA-256) revision. Since the introduction of `OCIRepository` and with the recent changes around `HelmChart` objects to allow the consumption of charts from OCI registries, this could be seen as outdated or confusing due to the format differing from the canonical format used by OCI, which is `@:` (the part after `@` formally known as a ["digest"][digest-spec]) to refer to a specific version of an OCI manifest. While also taking note that Git does not have an official canonical format for e.g. branch references at a specific commit, and `/` has less of a symbolic meaning than `@`, which could be interpreted as "`` _at_ ``". In addition, with the introduction of algorithm prefixes for an `Artifact`'s checksum, it would be possible to add support and allow user configuration of other algorithms than SHA-256. For example SHA-384 and SHA-512, or the more performant (parallelizable) [BLAKE3][]. Besides this, it would make it easier to implement a client that can verify the checksum without having to resort to an assumed format or guessing method based on the length of it, and allows for a more robust solution in which it can continue to calculate against the algorithm of a previous configuration. The inclusion of the `Artifact`'s algorithm prefix has been proposed before in [source-controller#855](https://github.com/fluxcd/source-controller/issues/855), with supportive response from Core Maintainers. ### Goals - Establish a canonical format to refer to an `Artifact`'s `Revision` field which consists of a named pointer and a specific checksum reference. - Allow easier verification of the `Artifact`'s checksum by including an alias for the algorithm. - Deprecate the `Artifact`'s `Checksum` field in favor of the `Digest` field. - Allow configuration of the algorithm used to calculate the checksum of an `Artifact`. - Allow configuration of algorithms other than SHA-256 to calculate the `Digest` of an `Artifact`. - Allow compatibility with SemVer name references which might contain an `@` symbol already (e.g. `package@v1.0.0@sha256:...`, as opposed to OCI's `name:v1.0.0@sha256:...`). ### Non-Goals - Define a canonical format for an `Artifact`'s `Revision` field which contains a named pointer and a different reference than a checksum. ## Proposal ### Establish an Artifact Revision format Change the format of the `Revision` field of the `source.toolkit.fluxcd.io` Group's `Artifact` type across all `Source` kinds to contain an `@` separator opposed to `/`, and include the algorithm alias as a prefix to the checksum (creating a "digest"). ```text [ ] [ [ "@" ] ":" ] ``` Where `` is the name of e.g. a Git branch or OCI repository name, `` is the exact revision (e.g. a Git commit SHA or OCI manifest digest), and `` is the alias of the algorithm used to calculate the checksum (e.g. `sha256`). In case only a named pointer or digest is advertised, the `@` is omitted. For a `GitRepository`'s `Artifact` pointing towards an SHA-1 Git commit on branch `main`, the `Revision` field value would become: ```text main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f ``` For a `GitRepository`'s `Artifact` pointing towards a specific SHA-1 Git commit without a defined branch or tag, the `Revision` field value would become: ```text sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f ``` For a `Bucket`'s `Artifact` with a revision based on an SHA-256 calculation of a list of object keys and their etags, the `Revision` field value would become: ```text sha256:8fb62a09c9e48ace5463bf940dc15e85f525be4f230e223bbceef6e13024110c ``` For a `HelmChart`'s `Artifact` pointing towards a Helm chart version, the `Revision` field value would become: ```text 1.2.3 ``` ### Introduce a `Digest` field Introduce a new field to the `source.toolkit.fluxcd.io` Group's `Artifact` type across all `Source` kinds called `Digest`, containing the checksum of the file advertised in the `Path`, and alias of the algorithm used to calculate it (creating a ["digest"][digest-spec]). ```text ":" ``` For a `GitRepository` `Artifact`'s checksum calculated using SHA-256, the `Digest` field value would become: ```text sha256:1111f92aba67995f108b3ee3ffdc00edcfe206b11fbbb459c8ef4c4a8209fca8 ``` #### Deprecate the `Checksum` field In favor of the `Digest` field, the `Checksum` field of the `source.toolkit.fluxcd.io` Group's `Artifact` type across all `Source` kinds is deprecated, and removed in a future version. ### User Stories #### Artifact revision verification > As a user of the source-controller, I want to be able to see the exact > revision of an Artifact that is being used, so that I can verify that it > matches the expected revision at a remote source. For a Source kind that has an `Artifact` with a `Revision` which contains a checksum, the field value can be retrieved using the Kubernetes API. For example: ```console $ kubectl get gitrepository -o jsonpath='{.status.artifact.revision}' main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f ``` #### Artifact checksum verification > As a user of the source-controller, I want to be able to verify the checksum > of an Artifact. For a Source kind with an `Artifact` the digest consisting of the algorithm alias and checksum is advertised in the `Digest` field, and can be retrieved using the Kubernetes API. For example: ```console $ kubectl get gitrepository -o jsonpath='{.status.artifact.digest}' sha256:1111f92aba67995f108b3ee3ffdc00edcfe206b11fbbb459c8ef4c4a8209fca8 ``` #### Artifact checksum algorithm configuration > As a user of the source-controller, I want to be able to configure the > algorithm used to calculate the checksum of an Artifact. The source-controller binary accepts a `--artifact-digest-algo` flag which configures the algorithm used to calculate the checksum of an `Artifact`. The default value is `sha256`, but can be changed to `sha384`, `sha512` or `blake3`. When set, newly advertised `Artifact`'s `Digest` fields will be calculated using the configured algorithm. For previous `Artifact`'s that were set using a previous configuration, the `Artifact`'s `Digest` field will be recalculated using the advertised algorithm. #### Artifact revisions in notifications > As a user of the notification-controller, I want to be able to see the > exact revision a notification is referring to. The notification-controller can use the revision for a Source's `Artifact` attached as an annotation to an `Event`, and correctly parses the value field when attempting to extract e.g. a Git commit digest from an event for a `GitRepository`. As currently already applicable for the `/` separator. > As a user of the notification-controller, I want to be able to observe what > commit has been applied on my (supported) Git provider. The notification-controller can use the revision attached as an annotation to an `Event`, and is capable of extracting the correct reference for a Git provider integration (e.g. GitHub, GitLab) to construct a payload. For example, extracting `1eabc9a41ca088515cab83f1cce49eb43e84b67f` from `main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f`. #### Artifact revisions in listed views > As a Flux CLI user, I want to see the current revision of my Source in a > listed overview. By running `flux get source `, the listed view of Sources would show a truncated version of the checksum in the `Revision` field. ```console $ flux get source gitrepository NAME REVISION SUSPENDED READY MESSAGE flux-monitoring main@sha1:1eabc9a4 False True stored artifact for revision 'main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f' $ flux get source oci NAME REVISION SUSPENDED READY MESSAGE apps-source local@sha256:e5fa481b False True stored artifact for digest 'local@sha256:e5fa481bb17327bd269927d0a223862d243d76c89fe697ea8c9adefc47c47e17' $ flux get source bucket NAME REVISION SUSPENDED READY MESSAGE apps-source sha256:e3b0c442 False True stored artifact for revision 'sha256:8fb62a09c9e48ace5463bf940dc15e85f525be4f230e223bbceef6e13024110c' ``` ### Alternatives The two main alternatives around the `Revision` parts in this RFC are to either keep the current field value formats as is, or to invent another format. Given the [motivation](#motivation) for this RFC outlines the reasoning for not keeping the current `Revision` format, and the proposed is a commonly known format. Neither strike as a better alternative. For the changes related to `Checksum` and `Digest`, the alternative is to keep the current field name as is, and only change the field value format. However, given the naming of the field is more accurate with the introduction of the algorithm alias, and now is the time to make last (breaking) changes to the API. This does not strike as a better alternative. ## Design Details ### Artifact Revision format For an `Artifact`'s `Revision` which contains a checksum referring to an exact revision, the checksum within the value MUST be appended with an alias for the algorithm separated by `:` (e.g. `sha256:...`), further referred to as a "digest". The algorithm alias and checksum of the digest MUST be lowercase and alphanumeric. For an `Artifact`'s `Revision` which contains a digest and a named pointer, it MUST be prefixed with `@`, and appended at the end of the `Revision` value. The named pointer MAY contain arbitrary characters, including but not limited to `/` and `@`. #### Format ```text [ ] [ [ "@" ] ":" ] ``` Where `[ ]` indicates an optional element, `" "` a literal string, and `< >` a variable. #### Parsing When parsing the `Revision` field value of an `Artifact` to extract the digest, the value after the last `@` is considered to be the digest. The remaining value on the left side is considered to be the named pointer, which MAY contain an additional `@` separator if applicable for the domain of the `Source` implementation. #### Truncation When truncating the `Revision` field value of an `Artifact` to display in a view with limited space, the `` of the digest MAY be truncated to 7 or more characters. The `` of the digest MUST NOT be truncated. In addition, a digest MUST always contain the full length checksum for the algorithm. #### Backwards compatibility To allow backwards compatibility in the notification-controller, Flux CLI and other applicable components, the `Revision` new field value format could be detected by the presence of the `@` or `:` characters. Falling back to their current behaviour if not present, phasing out the old format in a future release. ### Artifact Digest The `Artifact`'s `Digest` field advertises the checksum of the file in the `URL`. The checksum within the value MUST be appended with an alias for the algorithm separated by `:` (e.g. `sha256:...`). This follows the [digest format][go-digest] of OCI. #### Format ```text ":" ``` Where `" "` indicates a literal string, and `< >` a variable. #### Library The library used for calculating the `Digest` field value is `github.com/opencontainers/go-digest`. This library is stable and extensible, and used by various OCI libraries which we already depend on. #### Calculation The checksum in the `Digest` field value MUST be calculated using the canonical algorithm [set at runtime](#configuration). #### Configuration The algorithm used for calculating the `Digest` field value MAY be configured using the `--artifact-digest-algo` flag of the source-controller binary. The default value is `sha256`, but can be changed to `sha384`, `sha512` or `blake3`. **Note:** availability of BLAKE3 is at present dependent on an explicit import of `github.com/opencontainers/go-digest/blake3`. When the provided algorithm is NOT supported, the source-controller MUST fail to start. When the configured algorithm changes, the `Digest` MAY be recalculated to update the value. #### Verification The checksum of a downloaded artifact MUST be verified against the `Digest` field value. If the checksum does not match, the verification MUST fail. ### Deprecation of Checksum The `Artifact`'s `Checksum` field is deprecated and MUST be removed in a future release. The `Digest` field MUST be used instead. #### Backwards compatibility To allow backwards compatibility, the source-controller could continue to advertise the checksum part of a `Digest` in the `Checksum` field until the field is removed. ## Implementation History * **2023-02-20** First implementation released with [flux2 v0.40.0](https://github.com/fluxcd/flux2/releases/tag/v0.40.0) [BLAKE3]: https://github.com/BLAKE3-team/BLAKE3 [digest-spec]: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests [go-digest]: https://pkg.go.dev/github.com/opencontainers/go-digest#hdr-Basics ================================================ FILE: rfcs/0006-cdevents/README.md ================================================ # RFC-0006 Flux CDEvents Receiver **Status:** implementable **Creation date:** 2023-12-08 **Last update:** 2024-03-13 ## Summary This RFC proposes to add a `Receiver` type to the Flux notification-controller API for handling [CDEvents](https://cdevents.dev/). For `Receiver` objects configured to accept CDEvents, notification-controller will verify the events sent to the receiver's webhook URL, check that their type matches the expected type, and trigger the reconciliation of the configured resources. ## Motivation CDEvents enables interoperability between CI/CD tools in a workflow, and Flux is a very popular continuous delivery tool, and consequently the CDF team received many questions about integrating CDEvents with Flux. ### Goals Allow Flux to receive CDEvents and trigger the reconciliation of resources based on the received events. ### Non-Goals Make the Flux controllers emit CDEvents. ## Proposal Add CDEvents to the list of available receivers in Flux notification-controller. Similar to other receivers such as GitHub, Flux users will be able to use `spec.events` in order to specify which event types the receiver will allow. The receiver will also verify using the [CDEvents Go SDK](https://github.com/cdevents/sdk-go) that the payload sent to the webhook URL is a valid CDEvent. ### User Stories Users of multiple CI/CD tools such as Tekton and Flux could use CDEvents as a way to enable interoperability. For example, a user may want a Flux resource to reconcile as part of a Tekton `pipeline`. The Tekton `pipeline` will fire off a CDEvent to the CloudEvents Broker. A subscription that the user will have set up externally, e.g. with the [knative broker](https://knative.dev/docs/eventing/brokers/), will then send a relevant CDEvent to the Flux webhook receiver endpoint. ![usecase](cdevents-flux-tekton.png) ### Alternatives Certain use cases for CDEvents could be done alternatively using available receivers such as the generic webhook. ## Design Details Adding a Flux `Receiver` for CDEvents that works much like the other event-based receivers already implemented. The user will be able to define a Flux `Receiver` custom resource and deploy it to their cluster. The receiver takes the payload sent to the webhook URL by an external events broker, checks the headers for the event type, and filters out events based on the user-defined list of events in `spec.events`. If left empty, it will act on all valid CDEvents. It then validates the payload body using the [CDEvents Go SDK](https://github.com/cdevents/sdk-go). Valid events will then trigger the reconciliation of all Flux objects specified in `.spec.resources`. The CDEvents broker is not a part of this design and is left to the users to set up however they wish. Example Receiver: ```yaml apiVersion: notification.toolkit.fluxcd.io/v1 kind: Receiver metadata: name: cdevents-receiver namespace: flux-system spec: type: cdevents events: - "dev.cdevents.change.merged" secretRef: name: receiver-token resources: - apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository name: webapp namespace: flux-system ``` ![User Flowchart](Flux-CDEvents-RFC.png) ![Adapter](CDEvents-Flux-RFC-Adapter.png) ## Implementation History ================================================ FILE: rfcs/0007-git-repo-passwordless-auth/README.md ================================================ # RFC-0007 Passwordless authentication for Git repositories **Status:** implementable **Creation date:** 2023-31-07 **Last update:** 2025-08-13 ## Summary Flux should provide a mechanism to authenticate against Git repositories without the use of passwords. This RFC proposes the use of alternative authentication methods like OIDC, OAuth2 and IAM to access Git repositories hosted on specific Git SaaS platforms and cloud providers. ## Motivation At the moment, Flux supports HTTP basic and bearer authentication. Users are required to create a Secret containing the username and the password/bearer token, which is then referred to in the GitRepository using `.spec.secretRef`. While this works fine, it has a couple of drawbacks: * Scalability: Each new GitRepository potentially warrants another credentials pair, which doesn't scale well in big organizations with hundreds of repositories with different owners, increasing the risk of mismanagement and leaks. * Identity: A username is associated with an actual human. But often, the repository belongs to a team of 2 or more people. This leads to a problem where teams have to decide whose credentials should Flux use for authentication. These problems exist not due to flaws in Flux, but because of the inherent nature of password based authentication. With support for OIDC, OAuth2 and IAM based authentication, we can eliminate these problems: * Scalability: Since OIDC is fully handled by the cloud provider, it eliminates any user involvement in managing credentials. For OAuth2 and IAM, users do need to provide certain information like the ID of the resource, private key, etc. but these are still a better alternative to passwords since the same resource can be reused by multiple teams with different members. * Identity: Since all the above authentication methods are associated with a virtual resource independent of a user, it solves the problem of a single person being tied to automation that several people are involved in. ### Goals * Integrate with major cloud providers' OIDC and IAM offerings to provide a seamless way of Git repository authentication. * Integrate with major Git SaaS providers to support their app based OAuth2 mechanism. ### Non-Goals * Replace the existing basic and bearer authentication API. ## Proposal A new string field `.spec.provider` shall be added to the `GitRepository` API. The field will be an enum with the following variants: * `generic` * `azure` * `gcp` * `github` * `gitlab` `.spec.provider` will be an optional field which defaults to `generic` indicating that the user wants to authenticate via HTTP basic/bearer auth or SSH by providing the existing `.spec.secretRef` field. The sections below define the behavior when `.spec.provider` is set to one of the other providers. ### Azure Git repositories hosted on Azure Devops can be accessed using [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops). Seamless access from Flux to Azure devops repository can be achieved through [Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet). The user creates a managed identity and establishes a federated identity between Flux service account and the managed identity. Flux service account is patched to add an annotation specifying the client id of the managed identity. Flux service account and deployments are patched with labels to use workload identity. The managed identity must have sufficient permissions to be able to access Azure Devops resources. This enables Flux pod to access the Git repository without the need for any credentials. ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - gotk-components.yaml - gotk-sync.yaml patches: - patch: |- apiVersion: v1 kind: ServiceAccount metadata: name: source-controller namespace: flux-system annotations: azure.workload.identity/client-id: labels: azure.workload.identity/use: "true" - patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: source-controller namespace: flux-system labels: azure.workload.identity/use: "true" spec: template: metadata: labels: azure.workload.identity/use: "true" ``` Example of using an Azure Devops repository with `azure` provider: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: azure-devops spec: interval: 1m url: https://dev.azure.com///_git/ ref: branch: master # notice the lack of secretRef provider: azure ``` ### GCP Git repositories hosted on Google Cloud Source Repositories can be accessed by Flux via a [GCP Service Account](https://cloud.google.com/iam/docs/service-account-overview). Workload Identity Federation for GKE is [unsupported](https://cloud.google.com/iam/docs/federated-identity-supported-services) for Cloud Source Repositories. The user must instead create the GCP Service Account and link it to the Flux service account in order to enable workload identity. In order to link the GCP Service Account to the Flux service account, the following patch must be applied: ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - gotk-components.yaml - gotk-sync.yaml patches: - patch: | apiVersion: v1 kind: ServiceAccount metadata: name: source-controller annotations: iam.gke.io/gcp-service-account: target: kind: ServiceAccount name: source-controller ``` The Service Account must have sufficient permissions to be able to access Google Cloud Source Repositories. The Cloud Source Repositories uses the `source.repos.get` permission to access the repository, which is under the `roles/source.reader` role. Take a look at [this guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for more information about setting up GKE Workload Identity. Example of using a Google Cloud Source Repository with `gcp` provider: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: gcp-repo spec: interval: 1m url: https://source.developers.google.com/p//r/ ref: branch: master provider: gcp ``` ### GitHub Git repositories hosted on GitHub can be accessed via [GitHub Apps](https://docs.github.com/en/apps/overview). This allows users to create a single resource from which they can access all their GitHub repositories. The app must have sufficient permissions to be able to access repositories. The app's ID, private key and installation ID should be mentioned in the Secret referred to by `.spec.secretRef`. GitHub Enterprise users will also need to mention their GitHub API URL in the Secret. Example of using a Github repository with `github` provider: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: github-repo spec: interval: 1m url: https://github.com// ref: branch: master provider: github secretRef: name: github-app --- kind: Secret metadata: name: github-app stringData: githubAppID: githubInstallationID: githubPrivateKey: | githubApiURl: #optional, required only for GitHub Enterprise users ``` ### Gitlab Git repositories hosted on Gitlab can be accessed via OAuth2 Gitlab Applications created from the [UI](https://docs.gitlab.com/ee/integration/oauth_provider.html) or using [API](https://docs.gitlab.com/ee/api/applications.html). The Gitlab Oauth2 application must be created with the required scope to access gitlab repositories. The application's `application_id`, `secret` and `redirect_uri` are used to request an [access token](https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow). These parameters are configured in the secret referred to by `.spec.secretRef`. Example of using gitlab repository with `gitlab` provider: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: gitlab-repo spec: interval: 1m url: https://gitlab.com// ref: branch: main provider: gitlab secretRef: name: gitlab-app --- kind: Secret metadata: name: gitlab-app stringData: gitlabAppID: gitlabAppSecret: gitlabAppRedirectUrl: ``` ### User Stories #### User Story 1 > As a user running flux controllers, deployed from a private repository in > a cloud provider that supports context-based authentication, I want to securely > authenticate to the repository without setting up secrets and having to manage > authentication tokens (refreshing, rotating, etc.). To enable this scenario, the user would enable context-based authentication in their cloud provider and integrate it with their kubernetes cluster. For example, in Azure, using AKS and Azure Devops, the user would create a managed identity and establish a federated identity between Flux service account and the managed identity. Flux would then be able to access the Git repository by requesting a token from the Azure service. The user would not need to create a secret or manage any tokens. #### User Story 2 > As a user running flux controllers, deployed from a private repository, I want > to configure authentication to the repository that is not associated to a > personal account and does not expire. To enable this scenario, the user would either enable context-based authentication in their cloud provider and integrate it with their kubernetes cluster, or set up an OAuth2 application in their Git SaaS provider and provide the OAuth2 application details (application ID, secret, redirect URL) in a kubernetes secret. Flux would then be able to access the Git repository by requesting a token from the cloud provider or Git SaaS provider. The user would not need to create any credentials tied to a personal account. ## Design Details Flux source controller uses `GitRepository` API to define a source to produce an Artifact for a Git repository revision. Flux image automation controller updates YAML files when new images are available and commits changes to a given Git repository. The `ImageUpdateAutomation` API defines an automation process that updates the Git repository referenced in it's `.spec.sourceRef`. If the new optional string field `.spec.provider` is specified in the `GitRepository` API, the respective provider is used to configure the authentication to check out the source for flux controllers. ### Azure If `.spec.provider` is set to `azure`, Flux controllers will use [DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) to build the workload identity credential. This credential type uses the environment variables injected by the Azure Workload Identity mutating webhook. The [access token from the credential will be then used as a bearer token](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-use-a-service-principal-to-do-git-operations-like-clone-a-repo) to perform HTTP bearer authentication. ### GCP If `.spec.provider` is set to `gcp`, Flux source controller will fetch the access token from the [GKE metadata server](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#metadata_server). The GKE metadata server runs as a DaemonSet, with one Pod on every Linux node or a native Windows service on every Windows node in the cluster. The metadata server intercepts HTTP requests to `http://metadata.google.internal`. The source controller will use the url `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token` to retrieve a token for the IAM service account that the Pod is configured to impersonate. This access token will be then used to perform HTTP basic authentication. ### GitHub If `.spec.provider` is set to `github`, Flux controllers will get the app details from the specified Secret and use it to [generate an app installation token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app). This token is then used as the password and [`x-access-token` as the username](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app#choosing-permissions-for-git-access) to perform HTTP basic authentication. ### Gitlab If `.spec.provider` is set to `gitlab`, Flux controllers will use the application_id, secret and redirect_url specified in `.spec.secret` to generate an access token. The git repository can then be accessed by specifying [oauth2 as the username and the access token as the password](https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token) to perform HTTP basic authentication. ## Implementation History * GitHub App authentication implemented and generally available in Flux v2.5. * Azure DevOps authentication implemented and generally available in Flux v2.4. ================================================ FILE: rfcs/0008-custom-event-metadata-from-annotations/README.md ================================================ # RFC-0008 Custom Event Metadata from Annotations **Status:** implemented **Creation date:** 2024-05-23 **Last update:** 2025-02-22 ## Summary Flux users often run into situations where they wish to send custom, static metadata fields defined in Flux objects on the events dispatched by the respective Flux controller to Kubernetes and notification-controller. This proposal offers a solution for supporting those use cases uniformly across all Flux controllers by sending the annotation keys in Flux objects that are prefixed with the API Group `event.toolkit.fluxcd.io` followed by a slash, i.e. `event.toolkit.fluxcd.io/`. After the overall outcome of this RFC is implemented, Flux would have customization options for notification metadata strong enough to eliminate the need for the `.spec.summary` field from the Alert API. During the discussion of this RFC the Flux team has decided to deprecate `.spec.summary` in favor of `.spec.eventMetadata.summary`, and to remove this field in the Flux release of Alert API v1 GA when it takes place. ## Motivation This RFC comes as a response to the need for adding custom metadata to events about Flux objects sent to notification providers. See specific user stories in the [User Stories](#user-stories) section. ### Goals Provide a method for Flux users to embed custom/static metadata in their Flux objects and have that metadata propagated to the notification providers. ### Non-Goals In this proposal we **do not** aim to provide a method for Flux users to send etcd-indexed custom metadata fields from Flux objects in events to notification-controller, most specifically labels. By design an event already contains enough identification information to locate the associated Flux object inside the cluster, which covers the use case of labels. Flux does not wish to incentivize practices that are impactful to clusters without a strong reason or benefit. ## Proposal When sending events about Flux objects, we propose sending annotation keys prefixed with the well-defined API Group `event.toolkit.fluxcd.io` followed by a slash, i.e. prefixed with `event.toolkit.fluxcd.io/`, in addition to all the metadata that is already sent in the event. ### User Stories #### Story 1 > As a user, I want to embed Flux into my GitHub Workflow in a way that it only succeeds if > the deployment made by Flux is successful. For example, embedding a Deployment ID from the GitHub API in a `HelmRelease` object like the one below: ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: flux-system annotations: event.toolkit.fluxcd.io/deploymentID: e076e315-5a48-41c3-81c8-8d8bdee7d74d spec: chart: spec: chart: podinfo version: 6.5.* sourceRef: kind: HelmRepository name: podinfo ``` Should cause notification-controller to propagate an event like the one below (most fields omitted for brevity): ```json { "involvedObject": { "apiVersion": "helm.toolkit.fluxcd.io/v2", "kind": "HelmRelease", "name": "podinfo", "namespace": "flux-system", "uid": "7d0cdc51-ddcf-4743-b223-83ca5c699632" }, "metadata": { "deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d" } } ``` #### Story 2 > As a user, I want to embed the new image tag in a `HelmRelease` object when the image is updated by an `ImageUpdateAutomation` > and have that information propagated to the notification providers. For example: ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: flux-system annotations: event.toolkit.fluxcd.io/image: ghcr.io/stefanprodan/podinfo:latest # {"$imagepolicy": "flux-system:podinfo"} spec: chart: spec: chart: podinfo sourceRef: kind: HelmRepository name: podinfo values: image: tag: latest # {"$imagepolicy": "flux-system:podinfo:tag"} ``` In this example image-automation-controller would update the image and tag near the markers. If, for example, it updates the image to `ghcr.io/stefanprodan/podinfo:6.5.0`, then it would cause notification-controller to start propagating events like the one below (most fields omitted for brevity): ```json { "involvedObject": { "apiVersion": "helm.toolkit.fluxcd.io/v2", "kind": "HelmRelease", "name": "podinfo", "namespace": "flux-system", "uid": "7d0cdc51-ddcf-4743-b223-83ca5c699632" }, "metadata": { "image": "ghcr.io/stefanprodan/podinfo:6.5.0" } } ``` ### Alternatives #### Alternative 1 An alternative for specifying custom metadata fields in Flux objects for sending on events is defining `.spec` APIs for such, like `.spec.eventMetadata` available in the Alert API. This alternative is not great because: * Such APIs would be fairly redundant with the well-known Kubernetes annotations. * Technically speaking, it is much easier to implement an alternative where the field storing the custom metadata is the same and is already available across all the Flux objects rather than introducing a new API. In the specific case of the Alert API this field was introduced because the Alert API is obviously a special one in the context of events and alerting. In particular, the Alert objects do not generate events themselves, but rather serve as an aggregation configuration for matching and propagating events from other Flux objects. #### Alternative 2 Instead of introducing a new API Group, i.e. `event.toolkit.fluxcd.io`, we could use the API Group `notification.toolkit.fluxcd.io` for the same purpose. This alternative is not great because it emphasizes an exclusive relationship with notification-controller, which is not the case. The events here are also Kubernetes Events, and an API Group that is more general and closer to Kubernetes Events is more appropriate. ## Design Details All the Flux controllers use our implementation of the `EventRecorder` interface from the Go package `k8s.io/client-go/tools/record`: [`(*github.com/fluxcd/pkg/runtime/event.Recorder).AnnotatedEventf()`](https://github.com/fluxcd/pkg/blob/6f2619522699f1a78e8c7b41583ad9f7b7c9544e/runtime/events/recorder.go#L119). This implementation sends the events to notification-controller and also calls the same method from an injected `EventRecorder`, which sends the events to Kubernetes. To support the use cases discussed here we would modify this implementation to look for annotations prefixed with `event.toolkit.fluxcd.io/` in the Flux objects and send them alongside the other metadata of the event. Here we are talking specifically about the object annotations retrieved from the Flux object itself, i.e. the first argument of the `AnnotatedEventf()` method: `object runtime.Object`. This implementation would not change the interface `EventRecorder` used by the controllers, so all we need to do is bump the Go package `github.com/fluxcd/pkg/runtime` across all controllers. On the notification-controller side we would start accepting metadata keys starting with this prefix and remove it before sending the metadata key-value pair to the notification providers. This is an important aspect of the implementation because notification-controller only accepts metadata keys that are prefixed with the Group of the respective API the involved Flux object belongs to, so we need to add an exception for the new prefix. The API Group `event.toolkit.fluxcd.io` would be introduced as a constant in the package `github.com/fluxcd/pkg/apis/event` with the name `Group`. This constant would be used in the package `github.com/fluxcd/pkg/runtime/event` and notification-controller for the implementation described above. ### Precedence Order After this change there would be four sources of metadata being sent on notifications. They are listed below with the proposed order of precedence, from lowest to highest: 1. User-defined metadata on Flux objects, set with the `event.toolkit.fluxcd.io/` prefix in the keys of the object's `.metadata.annotations`. 2. User-defined metadata on the Alert object, set with `.spec.eventMetadata`. 3. User-defined summary on the Alert object, set with `.spec.summary`. 4. Controller-defined metadata, set with the `.toolkit.fluxcd.io/` prefix in the metadata keys of the event payload. Upon any key conflicts when combining all these metadata, notification-controller would resolve them according to the precedence order specified above, print an `info` log and emit a Kubernetes Event containing all the key conflicts to warn the user and prompt them to change their configuration to remove those conflicts. #### Reasoning Controller-defined metadata has the highest precedence because it integrates with external systems, e.g. commit SHAs, digests, chart versions, etc. Alert-level metadata, i.e. `.spec.summary` and `.spec.eventMetadata`, are usually cluster-level, e.g. the cluster name, region, environment, etc. We don't want tenants overriding cluster-level metadata. User-defined metadata on Flux objects, whose use cases are described in the [User Stories](#user-stories) section, would usually be defined by cluster tenants. Hence it should not override cluster-level metadata. The `.spec.summary` field of the Alert API was the first introduced for supporting user-defined metadata. The value of this field is appended to the notification metadata with the key `summary`. Later, `.spec.eventMetadata` was introduced to enhance this capability by supporting structured user-defined metadata, i.e. the ability to specify keys other than `summary`. The latter is a generalization of the former, and, given also the overall strong customization options offered by Flux for notification metadata after the changes introduced in this RFC, the Flux team has decided to deprecate `.spec.summary` in favor of `.spec.eventMetadata.summary`, and to remove it in the Flux release of Alert API v1 GA when it takes place. Until then, we decided to keep the current priority of the field, which is higher than `.spec.eventMetadata.summary`. ### How can this feature be enabled / disabled? To enable the feature, use the `event.toolkit.fluxcd.io/` prefix in Flux object annotations, for example: * `event.toolkit.fluxcd.io/image: ghcr.io/stefanprodan/podinfo` * `event.toolkit.fluxcd.io/deploymentID: e076e315-5a48-41c3-81c8-8d8bdee7d74d` It's important to notice that not all Flux objects emit events, e.g. Alert and Provider objects. For a list of the Flux objects that emit events, see the kinds allowed on the `.spec.eventSources[].kind` field of the Alert API. To disable the feature, do not use `event.toolkit.fluxcd.io/` as a prefix in Flux object annotations. ## Implementation History * RFC implemented and generally available in Flux v2.5. ================================================ FILE: rfcs/0009-custom-health-checks/README.md ================================================ # RFC-0009 Custom Health Checks for Kustomization using Common Expression Language (CEL) **Status:** implemented **Creation date:** 2024-01-05 **Last update:** 2025-02-22 ## Summary This RFC proposes to extend the Flux `Kustomization` API with custom health checks for custom resources using the Common Expression Language (CEL). In order to provide flexibility, we propose to use CEL expressions for defining the conditions that need to be met in order to determine the health of a custom resource. We will introduce a new field called `healthCheckExprs` in the `Kustomization` CRD which will be a list of CEL expressions for evaluating the status of a particular Kubernetes resource kind. ## Motivation Flux uses the `kstatus` library during the health check phase to compute owned resources status. This works just fine for all the Kubernetes core resources and custom resources that comply with the `kstatus` conventions. There are cases where the status of a custom resource does not follow the `kstatus` conventions. For example, we might want to compute the status of a custom resource based on a condition other than `Ready`. This is the case for resources that perform intermediary patching, like `Certificate` from cert-manager, where one should look at the `Issuing` condition to know if the certificate is being issued or not before looking at the `Ready` condition. We need to provide a way for users to express the conditions that need to be met in order to determine the health of a custom resource. We seek a solution flexible enough to cover all possible use cases that does not require changing the Flux source code for each new CRD. ### Goals - Provide a generic solution for users to customise the health check evaluation of custom resources. - Provide a space for the community to contribute custom health checks for popular custom resources. ### Non-Goals - We do not plan to support custom health checks for Kubernetes core resources. ## Proposal ### Introduce a new field `HealthCheckExprs` in the `Kustomization` CRD The `HealthCheckExprs` field will be a list of `CustomHealthCheck` objects. The `CustomHealthCheck` object fields would be: `apiVersion`, `kind`, `inProgress`, `failed` and `current`. For example, consider the following `Certificate` resource: ```yaml --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: app-certificate spec: commonName: cert-manager-tls dnsNames: - app.ns.svc.cluster.local ipAddresses: - x.x.x.x isCA: true issuerRef: group: cert-manager.io kind: ClusterIssuer name: app-issuer secretName: app-tls-certs subject: organizations: - example.com ``` This `Certificate` resource will transition through the following `conditions`: `Issuing` and `Ready`. In order to compute the status of this resource, we need to look at both the `Issuing` and `Ready` conditions. The Flux `Kustomization` object used to apply the `Certificate` will look like this: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: certs spec: interval: 5m prune: true sourceRef: kind: GitRepository name: flux-system path: ./certs wait: true healthCheckExprs: - apiVersion: cert-manager.io/v1 kind: Certificate inProgress: "status.conditions.filter(e, e.type == 'Issuing').all(e, e.observedGeneration == metadata.generation && e.status == 'True')" failed: "status.conditions.filter(e, e.type == 'Ready').all(e, e.observedGeneration == metadata.generation && e.status == 'False')" current: "status.conditions.filter(e, e.type == 'Ready').all(e, e.observedGeneration == metadata.generation && e.status == 'True')" ``` The `.spec.healthCheckExprs` field contains an entry for the `Certificate` kind, its `apiVersion`, and the CEL expressions that need to be met in order to determine the health status of all custom resources of this kind reconciled by the Flux `Kustomization`. ### Custom Health Check Library To help users define custom health checks, we will provide on the [fluxcd.io](https://fluxcd.io) website a library of custom health checks for popular custom resources. The Flux community will be able to contribute to this library by submitting pull requests to the [fluxcd/website](https://github.com/fluxcd/website) repository. ### User Stories #### Configure health checks for non-standard custom resources > As a Flux user, I want to be able to specify health checks for > custom resources that don't have a Ready condition, so that I can be notified > when the status of my resources transitions to a failed state based on the evaluation > of a different condition. Using `.spec.healthCheckExprs`, Flux users have the ability to specify the conditions that need to be met in order to determine the health of a custom resource. This enables Flux to query any `.status` field, besides the standard `Ready` condition, and evaluate it using a CEL expression. Example for `SealedSecret` which has a `Synced` condition: ```yaml - apiVersion: bitnami.com/v1alpha1 kind: SealedSecret failed: "status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'False')" current: "status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'True')" ``` #### Use Flux dependencies for Kubernetes ClusterAPI > As a Flux user, I want to be able to use Flux dependencies bases on the > readiness of ClusterAPI resources, so that I can ensure that my applications > are deployed only when the ClusterAPI resources are ready. The ClusterAPI resources have a `Ready` condition, but this is set in the status after the cluster is first created. Given this behavior, at creation time Flux cannot find any condition to evaluate the status of the ClusterAPI resources, thus it considers them as static resources which are always ready. Using `.spec.healthCheckExprs`, Flux users can specify that the `Cluster` kind is expected to have a `Ready` condition which will force Flux into waiting for the ClusterAPI resources status to be populated. Example for `Cluster`: ```yaml - apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster failed: "status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'False')" current: "status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'True')" ``` ### Alternatives We need an expression language that is flexible enough to cover all possible use cases, without having to change Flux source code for each new use case. An alternative that has been considered was to use `CUE` instead of `CEL`. `CUE` lang is a more powerful expression language, but given the fact that Kubernetes makes use of `CEL` for CRD validation and admission control, we have decided to also use `CEL` in Flux in order to be consistent with the Kubernetes ecosystem. ## Design Details ### Introduce a new field `HealthCheckExprs` in the `Kustomization` CRD The `api/v1/kustomization_types.go` file will be updated to add the `HealthCheckExprs` field to the `KustomizationSpec` struct. ```go type KustomizationSpec struct { // +optional HealthCheckExprs []CustomHealthCheck `json:"healthCheckExprs,omitempty"` } type CustomHealthCheck struct { // APIVersion of the custom resource under evaluation. // +required APIVersion string `json:"apiVersion"` // Kind of the custom resource under evaluation. // +required Kind string `json:"kind"` HealthCheckExpressions `json:",inline"` } type HealthCheckExpressions struct { // Current is the CEL expression that determines if the status // of the custom resource has reached the desired state. // +required Current string `json:"current"` // InProgress is the CEL expression that determines if the status // of the custom resource has not yet reached the desired state. // +optional InProgress string `json:"inProgress,omitempty"` // Failed is the CEL expression that determines if the status // of the custom resource has failed to reach the desired state. // +optional Failed string `json:"failed,omitempty"` } ``` If a CEL expression evaluation results in an error, for example, looking for a field that does not exist, the health check will fail. Users will be encouraged to test their expressions in the [CEL Playground](https://playcel.undistro.io/). Here is where the community-maintained [library](#custom-health-check-library) will be super useful as some of the expressions might be complex. The evaluation logic will be as follows. First, we check if the custom resource has a `.status.observedGeneration` field, if it does we compare it with the `.metadata.generation` field to determine if the custom resource is in progress. We consider it in progress if these fields differ, and don't evaluate any of the expressions if that's the case. However, if these fields are equal there's no immediate conclusion about the health of the custom resource, so we proceed with the evaluation. For each of the `InProgress`, `Failed` and `Current` expressions, we evaluate the expressions that are specified (`InProgress` and `Failed` are optional) in this specific order and return the respective status of the first expression that evaluates to `true`. If none of the expressions evaluate to `true`, we consider the custom resource in progress. When the `Failed` expression is not specified the controller will keep evaluating the `Current` expression until it returns `true`, and will give up after the timeout defined in the Kustomization's `spec.timeout` field is reached. Users will be encouraged to provide a `Failed` expression to avoid stalling the reconciliation loop until the timeout is reached. ### Introduce a generic custom status reader We'll Introduce a `StatusReader` that will be used to compute the status of custom resources based on the `CEL` expressions provided in the `CustomHealthCheck`: ```go import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" kstatusreaders "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) type CELStatusReader struct { genericStatusReader engine.StatusReader gvk schema.GroupVersionKind } func NewCELStatusReader(mapper meta.RESTMapper, gvk schema.GroupVersionKind, exprs *kustomizev1.HealthCheckExpressions) engine.StatusReader { genericStatusReader := kstatusreaders.NewGenericStatusReader(mapper, genericConditions(gvk.Kind, exprs)) return &CELStatusReader{ genericStatusReader: genericStatusReader, gvk: gvk, } } func (g *CELStatusReader) Supports(gk schema.GroupKind) bool { return gk == g.gvk.GroupKind() } func (g *CELStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) { return g.genericStatusReader.ReadStatus(ctx, reader, resource) } func (g *CELStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) { return g.genericStatusReader.ReadStatusForObject(ctx, reader, resource) } ``` The `genericConditions` function takes the set of `CEL` expressions and returns a function that takes an `Unstructured` object and returns a `status.Result` object. ```go import ( "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/fluxcd/pkg/runtime/cel" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func genericConditions(exprs *kustomizev1.HealthCheckExpressions) func(u *unstructured.Unstructured) (*status.Result, error) { return func(u *unstructured.Unstructured) (*status.Result, error) { obj := u.UnstructuredContent() // if status.observedGeneration exists and differs from metadata.generation return status.InProgress for _, e := range []struct{ expr string status status.Status }{ {expr: exprs.InProgress, status: status.InProgress}, {expr: exprs.Failed, status: status.Failed}, {expr: exprs.Current, status: status.Current}, } { if e.expr != "" { result, err := cel.EvaluateBooleanExpr(e.expr, obj) if err != nil { return nil, err } if result { return &status.Result{Status: e.status}, nil } } } return &status.Result{Status: status.InProgress}, nil } } ``` The CEL status reader will be used by the `statusPoller` provided to the kustomize-controller `reconciler` to compute the status of the resources for the registered custom resources GVKs. We will implement a `CEL` environment that will use the Kubernetes CEL library to evaluate the `CEL` expressions. ## Implementation History * RFC implemented and generally available in Flux v2.5. ================================================ FILE: rfcs/0010-multi-tenant-workload-identity/README.md ================================================ # RFC-0010 Multi-Tenant Workload Identity **Status:** implemented **Creation date:** 2025-02-22 **Last update:** 2026-03-13 ## Summary In this RFC we aim to add support for multi-tenant workload identity in Flux, i.e. the ability to specify at the object-level which set of cloud provider permissions must be used for interacting with the respective cloud provider on behalf of the reconciliation of the object. In this process, credentials must be obtained automatically, i.e. this feature must not involve the use of secrets. This would be useful in a number of Flux APIs that need to interact with cloud providers, spanning all the Flux controllers. ### Multi-Tenancy Model In the context of this RFC, multi-tenancy refers to the ability of a single Flux instance running inside a Kubernetes cluster to manage Flux objects belonging to all the tenants in the cluster while still ensuring that each tenant has access only to their own resources according to the Least Privilege Principle. In this scenario a tenant is often a team inside an organization, so the reader can consider the [multi-team tenancy model](https://kubernetes.io/docs/concepts/security/multi-tenancy/#multiple-teams). Each team has their own namespaces, which are not shared with other teams. ## Motivation Flux has strong multi-tenancy features. For example, the `Kustomization` and `HelmRelease` APIs support the field `spec.serviceAccountName` for specifying the Kubernetes `ServiceAccount` to impersonate when interacting with the Kubernetes API on behalf of a tenant, e.g. when applying resources. This allows tenants to be constrained under the Kubernetes RBAC permissions granted to this `ServiceAccount`, and therefore have access only to the specific subset of resources they should be allowed to use. Besides the Kubernetes API, Flux also interacts with cloud providers, e.g. container registries, object storage, pub/sub services, etc. In these cases, Flux currently supports basically two modes of authentication: - *Secret-based multi-tenant authentication*: Objects have the field `spec.secretRef` for specifying the Kubernetes `Secret` containing the credentials to use when interacting with the cloud provider. This is similar to the `spec.serviceAccountName` field, but for cloud providers. The problem with this approach is that secrets are a security risk and operational burden, as they must be managed and rotated. - *Workload-identity-based single-tenant authentication*: Flux offers single-tenant workload identity support by configuring the `ServiceAccount` of the Flux controllers to impersonate a cloud identity. This eliminates the need for secrets, as the credentials are obtained automatically by the cloud provider Go libraries used by the Flux controllers when they are running inside the respective cloud environment. The problem with this approach is that it is single-tenant, i.e. all objects are reconciled using the same cloud identity, the one associated with the respective controller. For delivering the high level of security and multi-tenancy support that Flux aims for, it is necessary to extend the workload identity support to be multi-tenant. This means that each object must be able to specify which cloud identity must be impersonated when interacting with the cloud provider on behalf of the reconciliation of the object. This would allow tenants to be constrained under the cloud provider permissions granted to this identity, and therefore have access only to the specific subset of resources they are allowed to manage. ### Goals Provide multi-tenant workload identity support in Flux, i.e. the ability to specify at the object-level which cloud identity must be impersonated to interact with the respective cloud provider on behalf of the reconciliation of the object, without the need for secrets. ### Non-Goals It's not a goal of this RFC to implement an identity provider for Flux. Instead, the goal is to leverage Kubernetes' built-in identity provider capabilities, i.e. the Kubernetes `ServiceAccount` token issuer, to obtain short-lived access tokens for the cloud providers. ## Proposal For supporting multi-tenant workload identity at the object-level for the Flux APIs we propose associating the Flux objects with Kubernetes `ServiceAccounts`. The controller would need to create a token for the `ServiceAccount` associated with the object in the Kubernetes API, and then exchange it for a short-lived access token for the cloud provider. This would require the controller `ServiceAccount` to have RBAC permission to create tokens for any `ServiceAccounts` in the cluster. ### User Stories #### Story 1 > As a cluster administrator, I want to allow tenant A to pull OCI artifacts > from the Amazon ECR repository belonging to tenant A, but only from this > repository. At the same time, I want to allow tenant B to pull OCI artifacts > from the Amazon ECR repository belonging to tenant B, but only from this > repository. For example, I would like to have the following configuration: ```yaml apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: name: tenant-a-repo namespace: tenant-a spec: ... provider: aws serviceAccountName: tenant-a-ecr-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-ecr-sa namespace: tenant-a annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789123:role/tenant-a-ecr --- apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: OCIRepository metadata: name: tenant-b-repo namespace: tenant-b spec: ... provider: aws serviceAccountName: tenant-b-ecr-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-ecr-sa namespace: tenant-b annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789123:role/tenant-b-ecr ``` #### Story 2 > As a cluster administrator, I want to allow tenant A to pull and push to the Git > repository in Azure DevOps belonging to tenant A, but only this repository. At > the same time, I want to allow tenant B to pull and push to the Git repository > in Azure DevOps belonging to tenant B, but only this repository. For example, I would like to have the following configuration: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: tenant-a-repo namespace: tenant-a spec: ... provider: azure serviceAccountName: tenant-a-azure-devops-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-azure-devops-sa namespace: tenant-a annotations: azure.workload.identity/client-id: d6e4fc00-c5b2-4a72-9f84-6a92e3f06b08 # client ID for my tenant A azure.workload.identity/tenant-id: 72f988bf-86f1-41af-91ab-2d7cd011db47 # azure tenant for the cluster (optional, defaults to the env var AZURE_TENANT_ID set in the controller) --- apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation metadata: name: tenant-a-image-update namespace: tenant-a spec: ... sourceRef: kind: GitRepository name: tenant-a-repo --- apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: tenant-b-repo namespace: tenant-b spec: ... provider: azure serviceAccountName: tenant-b-azure-devops-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-azure-devops-sa namespace: tenant-b annotations: azure.workload.identity/client-id: 4a7272f9-f186-41af-9f84-6a92e32d7cd0 # client ID for my tenant B azure.workload.identity/tenant-id: 72f988bf-86f1-41af-91ab-2d7cd011db47 # azure tenant for the cluster (optional, defaults to the env var AZURE_TENANT_ID set in the controller) --- apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation metadata: name: tenant-b-image-update namespace: tenant-b spec: ... sourceRef: kind: GitRepository name: tenant-b-repo ``` #### Story 3 > As a cluster administrator, I want to allow tenant A to pull manifests from > the GCS bucket belonging to tenant A, but only from this bucket. At the same > time, I want to allow tenant B to pull manifests from the GCS bucket > belonging to tenant B, but only from this bucket. For example, I would like to have the following configuration: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: Bucket metadata: name: tenant-a-bucket namespace: tenant-a spec: ... provider: gcp serviceAccountName: tenant-a-gcs-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-gcs-sa namespace: tenant-a annotations: iam.gke.io/gcp-service-account: tenant-a-bucket@my-org-project.iam.gserviceaccount.com --- apiVersion: source.toolkit.fluxcd.io/v1 kind: Bucket metadata: name: tenant-b-bucket namespace: tenant-b spec: ... provider: gcp serviceAccountName: tenant-b-gcs-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-gcs-sa namespace: tenant-b annotations: iam.gke.io/gcp-service-account: tenant-b-bucket@my-org-project.iam.gserviceaccount.com ``` #### Story 4 > As a cluster administrator, I want to allow tenant A to decrypt secrets using > the AWS KMS key belonging to tenant A, but only this key. At the same time, > I want to allow tenant B to decrypt secrets using the AWS KMS key belonging > to tenant B, but only this key. For example, I would like to have the following configuration: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: tenant-a-aws-kms namespace: tenant-a spec: ... decryption: provider: sops serviceAccountName: tenant-a-aws-kms-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-aws-kms-sa namespace: tenant-a annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789123:role/tenant-a-kms --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: tenant-b-aws-kms namespace: tenant-b spec: ... decryption: provider: sops serviceAccountName: tenant-b-aws-kms-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-aws-kms-sa namespace: tenant-b annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789123:role/tenant-b-kms ``` #### Story 5 > As a cluster administrator, I want to allow tenant A to publish notifications > to the `tenant-a` topic in Google Cloud Pub/Sub, but only to this topic. At > the same time, I want to allow tenant B to publish notifications to the > `tenant-b` topic in Google Cloud Pub/Sub, but only to this topic. I want > to do so without creating any GCP IAM Service Accounts. For example, I would like to have the following configuration: ```yaml apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: tenant-a-google-pubsub namespace: tenant-a spec: ... type: googlepubsub serviceAccountName: tenant-a-google-pubsub-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-google-pubsub-sa namespace: tenant-a --- apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: tenant-b-google-pubsub namespace: tenant-b spec: ... type: googlepubsub serviceAccountName: tenant-b-google-pubsub-sa --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-google-pubsub-sa namespace: tenant-b ``` #### Story 6 > As a cluster administrator, I want to allow tenant A to use a GCP > Service Account to apply resources in a remote GKE cluster with > Kubernetes RBAC permissions granted to this GCP Service Account, > and tenant B to do the same using a different GCP Service Account. For example, I would like to have the following configuration: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: tenant-a-gke namespace: tenant-a spec: ... kubeConfig: provider: gcp serviceAccountName: tenant-a-gke-sa cluster: projects//locations//clusters/ --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-a-gke-sa namespace: tenant-a --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: tenant-b-gke namespace: tenant-b spec: ... kubeConfig: provider: gcp serviceAccountName: tenant-b-gke-sa cluster: projects//locations//clusters/ --- apiVersion: v1 kind: ServiceAccount metadata: name: tenant-b-gke-sa namespace: tenant-b ``` ### Alternatives #### An alternative for identifying Flux resources in cloud providers Instead of issuing `ServiceAccount` tokens in the Kubernetes API we could come up with a username naming scheme for Flux resources and issue tokens for these usernames instead, e.g. `flux:::`. This would make each Flux object have its own identity instead of using `ServiceAccounts` for this purpose. This choice would then prevent cases of other Flux objects from malicious actors in the same namespace from abusing the permissions granted to the `ServiceAccount` of the object. This choice, however, would provide a worse user experience, as Flux and Kubernetes users are already used to the `ServiceAccount` resource being the identity for resources in the cluster, not only in the context of plain RBAC but also in the context of workload identity. This choice would also require the introduction of new APIs for configuring the respective cloud identities in the Flux objects, when such APIs already exist as defined by the cloud providers themselves as annotations in the `ServiceAccount` resources. We therefore choose to stick with the well-known pattern of using `ServiceAccounts` for configuring the identities of the Flux resources. Furthermore, as mentioned in the [Multi-Tenancy Model](#multi-tenancy-model) section, the tenant trust domains are namespaces, so a tenant is expected to control and have access to all the resources `ServiceAccounts` in their namespaces are allowed to access. #### Alternatives for modifying controller RBAC to create `ServiceAccount` tokens In this section we discuss alternatives for changing the RBAC of controllers for creating `ServiceAccount` tokens cluster-wide, as it has a potential impact on the security posture of Flux. 1. We grant RBAC permissions to the `ServiceAccounts` of the Flux controllers (that would implement multi-tenant workload identity) for creating tokens for any other `ServiceAccounts` in the cluster. 2. We require users to grant "self-impersonation" to the `ServiceAccounts` so they can create tokens for themselves. The controller would then impersonate the `ServiceAccount` when creating a token for it. This operation would then only succeed if the `ServiceAccount` has been correctly granted permission to create a token for itself. In both alternatives the controller `ServiceAccount` would require some form of cluster-wide impersonation permission. Alternative 2 requires impersonation permission to be granted directly to the controller `ServiceAccount`, while in alternative 1, impersonation permission would be indirectly granted by the process of creating a token for another `ServiceAccount`. By creating a token for another `ServiceAccount`, the controller `ServiceAccount` effectively has the same permissions as the `ServiceAccount` it is creating the token for, as it could simply use the token to impersonate the `ServiceAccount`. Therefore it is reasonable to affirm that both alternatives are equivalent in terms of security. To break the tie between the two alternatives we introduce the fact that alternative 1 eliminates operational burden on users. In fact, native workload identity for pods does not require users to grant this self-impersonation permission to the `ServiceAccounts` of the pods. We therefore choose alternative 1. ## Design Details For detailing the proposal we need to first introduce the technical background on how workload identity is implemented by the managed Kubernetes services from the cloud providers. ### Technical Background Workload identity in Kubernetes is based on [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) (OIDC). The *Kubernetes `ServiceAccount` token issuer*, included as the `iss` JWT claim in the issued tokens, and represented by the default URL `https://kubernetes.default.svc.cluster.local`, implements the OIDC discovery protocol. Essentially, this means that the Kubernetes API will respond requests to the URL `https://kubernetes.default.svc.cluster.local/.well-known/openid-configuration` with a JSON document similar to the one below: ```json { "issuer": "https://kubernetes.default.svc.cluster.local", "jwks_uri": "https://172.18.0.2:6443/openid/v1/jwks", "response_types_supported": [ "id_token" ], "subject_types_supported": [ "public" ], "id_token_signing_alg_values_supported": [ "RS256" ] } ``` And to the URL `https://172.18.0.2:6443/openid/v1/jwks`, *discovered* through the field `.jwks_uri` in the JSON response above, the Kubernetes API will respond a JSON document similar to the following: ```json { "keys": [ { "use": "sig", "kty": "RSA", "kid": "NWm3YKmazJPVP7tttzkmSxUn0w8LGGp7yS2CanEF-A8", "alg": "RS256", "n": "lV2tbw9hnz1mseah2kMQNe5sRju4mPLlK0F7np97lLNC49G8yc5TMjyciLF3qsDNFCfWyYmsuGlcRg2BIBBX_jkpIUUjlsktdHhuqO2RnOqyRtNuljlT_b0QJgpgxCqq0DHI31EBc0JALOVd6EjjlhsVvVzZOw_b9KBXVS3D3RENuT0_FWauDq5NYbyYnjlvk-vUXCRMNDQSDNwx6X6bktwsmeDRXtM_bP3DokmnMYc4n0asTEg14L6VKky0ByF88Wi1-y0Pm0BHdobDGt1cIeUDeThk4E79JCHxkT5urAyYHcNwcfU4q-tnD6bTpNkFVsk3cqqK2nF7R_7ac5arSQ", "e": "AQAB" } ] } ``` This JSON document contains the public keys for verifying the signature of the issued tokens. By querying these two URLs in sequence, cloud providers are able to fetch the information required for verifying and trusting the tokens issued by the Kubernetes API. Most specifically, for trusting the `sub` JWT claim, which contains the Kubernetes `ServiceAccount` reference (name and namespace) for which the token was issued for, i.e. the `ServiceAccount` properly said. By allowing permissions to be granted to `ServiceAccounts` in the cloud provider, the cloud provider is then able to allow Kubernetes `ServiceAccounts` to access its resources. This is usually done by a *Security Token Service* (STS) that exchanges the Kubernetes token for a short-lived cloud provider access token, which is then used to access the cloud provider resources. It's important to mention that the Kubernetes `ServiceAccount` token issuer URL must be trusted by the cloud provider, i.e. users must configure this URL as a trusted identity provider. This process forms the basis for workload identity in Kubernetes. As long as the issuer URL can be reached by the cloud provider, this process can take place successfully. The reachability of the issuer URL by the cloud provider is where the implementation of workload identity starts to differ between cloud providers. For example, in GCP one can configure the content of the JWKS document directly in the GCP IAM console, which eliminates the need for network calls to the Kubernetes API. In AWS, on the other hand, this is not possible, the process has to be followed strictly, i.e. the issuer URL must be reachable by the AWS STS service. Furthermore, GKE automatically creates the necessary trust relationship between the Kubernetes issuer and the GCP STS service (i.e. automatically injects the JWKS document of the GKE cluster in the STS database), while in EKS this must be done manually by users (an OIDC provider must be created for each EKS cluster). Another difference is that the issuer URL remains the default/private one in GKE, while in EKS it is automatically set to a public one. This is done through the `--service-account-issuer` flag in the `kube-apiserver` command line arguments ([docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery)). This is a nice feature, as it allows external systems to federate access for workloads running in EKS clusters, e.g. EKS workloads can have federated access to GCP resources. Yet another difference between cloud providers that sheds light in our proposal is how applications running inside pods from the managed Kubernetes services obtain the short-lived cloud provider access tokens. In GCP, the GCP libraries used by the applications attempt to retrieve tokens from the *metadata server*, which is reachable by all pods running in GKE. This server creates a token for the `ServiceAccount` of the calling pod in the Kubernetes API, exchanges it for a short-lived GCP access token, and returns it to the application. In AKS, on the other hand, pods are mutated to include a [*token volume projection*](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection). The kubelet mounts and automatically rotates a volume with a token file inside the pod. The Azure libraries used by the applications then read this file periodically to perform the token exchange with the Azure STS service. Another aspect of workload identity that is important for this RFC is how the cloud identities are associated with the Kubernetes `ServiceAccounts`. In most cases, an identity from the IAM service of the cloud provider (e.g. a GCP IAM Service Account, or an AWS IAM Role) is associated with a Kubernetes `ServiceAccount` by the process of *impersonation*. Permission to impersonate the cloud identity is granted to the `ServiceAccount` through a configuration that points to the fully qualified name of the Kubernetes `ServiceAccount`, i.e. the name and namespace of the `ServiceAccount` and which cluster it belongs to in the name/address system of the cloud provider. Because the cloud provider needs to support this impersonation permission, some cloud providers go further and even remove the impersonation requirement, by allowing permissions to be granted directly to `ServiceAccounts` (if it needs to support granting the impersonation permission, then it can probably also easily support granting any other permissions depending on the implementation). GCP for example has implemented this feature [recently](https://cloud.google.com/blog/products/identity-security/make-iam-for-gke-easier-to-use-with-workload-identity-federation), a GCP IAM Service Account is no longer required for workload identity, i.e. GCP IAM permissions can now be granted directly to Kubernetes `ServiceAccounts`. This is a significant improvement in the user experience, as it significantly reduces the required configuration steps. AWS implemented a similar feature called *EKS Pod Identity*, but it still requires an IAM Role to be associated with the `ServiceAccount`. The minor improvement from the user experience perspective is that this association is implemented entirely in the AWS EKS/IAM APIs, no annotations are required in the Kubernetes `ServiceAccount`. Another improvement from this EKS feature compared to *IAM Roles for Service Accounts* is that users no longer need to create an *OIDC Provider* for the EKS cluster in the IAM API. It should be noted, however, that in the feature proposed here we cannot support *EKS Pod Identity* because it requires the `ServiceAccount` token to be bound to a pod, and the Kubernetes API will only issue a `ServiceAccount` token bound to a pod if that pod uses the respective `ServiceAccount`, which is a requirement we simply cannot meet. The only pod guaranteed to exist from the perspective of a Flux controller is itself. Therefore, it's impossible to support EKS Pod Identity for multi-tenant workload identity. (It is already possible to use it for single-tenant workload identity, though.) In sight of the technical background presented above, our proposal becomes simpler. The only solution to support multi-tenant workload identity at the object-level for the Flux APIs is to associate the Flux objects with Kubernetes `ServiceAccounts`. We propose building the `ServiceAccount` token creation and exchange logic into the Flux controllers through a library in the `github.com/fluxcd/pkg` repository. ### API Changes and Feature Gates For all the Flux APIs interacting with cloud providers (except `Kustomization`, see the paragraph below), we propose introducing the field `spec.serviceAccountName` (if not already present) for specifying the Kubernetes `ServiceAccount` on the same namespace of the object that must be used for getting access to the respective cloud resources. This field would be optional, and when not present the original behavior would be observed, i.e. the feature only activates when the field is present and a cloud provider among `aws`, `azure` or `gcp` is specified in the `spec.provider` field. So if only the `spec.provider` field is present and set to a cloud provider, then the controller would use single-tenant workload identity as it would prior to the implementation of this RFC, i.e. it would use its own identity for the operation. Note that this RFC does not seek to change the behavior when `spec.provider` is set to `generic` (or left empty, when it defaults to `generic`), in which case the field `spec.secretRef` can be used for specifying the Kubernetes `Secret` containing the credentials (or `spec.serviceAccountName` in the case of the APIs dealing with container registries, through the `imagePullSecrets` field of the `ServiceAccount`). The `Kustomization` API uses Key Management Services (KMS) for decrypting SOPS-encrypted secrets. We propose adding the dedicated optional field `spec.decryption.serviceAccountName` for multi-tenant workload identity when intercting with the KMS service. We choose having a dedicated field for the `Kustomization` API because the field `spec.serviceAccountName` already exists and is used for a major part of the functionality which is authenticating with the Kubernetes API when applying resources. If we used the same field for both purposes users would be forced to use multi-tenancy for both cloud and Kubernetes API interactions. Furthermore, the cloud provider in the `Kustomization` API is detected by the SOPS SDK itself while decrypting the secrets, so we don't need to introduce `spec.decryption.provider` for this purpose. The `Kustomization` and `HelmRelease` APIs have the field `spec.kubeConfig.secretRef` for specifying a Kubernetes `Secret` containing a static kubeconfig for accessing a remote Kubernetes cluster. We propose adding `spec.kubeConfig.configMapRef` for specifying a Kubernetes `ConfigMap` that is mutually exclusive with `spec.kubeConfig.secretRef` for supporting workload identity for both managed Kubernetes services from the cloud providers and also a `generic` provider. The fields in the `ConfigMap` would be the following: - `data.provider`: The provider to use for obtaining the temporary `*rest.Config` for the remote cluster. One of `generic`, `aws`, `azure` or `gcp`. Required. - `data.cluster`: Used only by `aws`, `azure` and `gcp`. The fully qualified name of the cluster resource in the respective cloud provider API. Needed for obtaining the unspecified fields `data.address` and `data["ca.crt"]` (not required if both are specified). - `data.address`: The HTTPS address of the API server of the remote cluster. Required for `generic`, optional for `aws`, `azure` and `gcp`. - `data.serviceAccountName`: The optional Kubernetes `ServiceAccount` to use for obtaining access to the remote cluster, implementing object-level workload identity. If not specified, the controller identity will be used. - `data.audiences`: The audiences Kubernetes `ServiceAccount` tokens must be issued for as a list of strings in YAML format. Optional. Defaults to `data.address` for `generic`, and has hardcoded default/specific values for `aws`, `azure` and `gcp` depending on the provider. - `data["ca.crt"]`: The optional PEM-encoded CA certificate of the remote cluster. For remote cluster access, the configured identity, be it controller-level or object-level, must have the necessary permissions to: - Access the cluster resource in the cloud provider API to get the cluster CA certificate and the cluster API server address. This is only necessary if one of `data.address` or `data["ca.crt"]` is not specified in the `ConfigMap`. In other words, at least two of the three fields `data.address`, `data["ca.crt"]` and `data.cluster` must be specified. If both `data.address` and `data["ca.crt"]` are specified, then the `data.cluster` field *must not* be specified, the controller will error out if it is. If only `data.cluster` and `data.address` are specified, then `data.address` has to match at least one of the addresses of the cluster resource in the cloud provider API. If only `data.cluster` and `data["ca.crt"]` are specified, then the first address of the cluster resource in the cloud provider API will be used as the address of the remote cluster and the CA returned by the cloud provider API will be ignored. If only `data.cluster` is specified, then the first address of the cluster resource in the cloud provider API will be used. - The relevant permissions for applying and managing the target resources in the remote cluster. For cloud providers this means either Kubernetes RBAC or the cloud provider API permissions, as managed Kubernetes services support authorizing requests through both ways. - When used with `spec.serviceAccountName`, the authenticated identity must have the necessary permissions to impersonate this `ServiceAccount` in the remote cluster (related [bug](https://github.com/fluxcd/pkg/issues/959)). To enable using the new `serviceAccountName` fields, we propose introducing a feature gate called `ObjectLevelWorkloadIdentity` in the controllers that would support the feature. In the first release we should make it opt-in so cluster admins can consciously roll it out. If the feature gate is disabled and users set the field a terminal error should be returned. ### Workload Identity Library We propose using the Go package `github.com/fluxcd/pkg/auth` for implementing a workload identity library that can be used by all the Flux controllers that need to interact with cloud providers. This library would be responsible for creating the `ServiceAccount` tokens in the Kubernetes API and exchanging them for short-lived access tokens for the cloud provider. The library would also be responsible for caching the tokens when configured by users. The library should support both single-tenant and multi-tenant workload identity because single-tenant implementations are already supported in GA APIs and hence they must remain available for backwards compatibility. Furthermore, it would be easier to support both use cases in a single library as opposed to mingling a new library into the currently existing ones, so this new library becomes the definitive unified solution for workload identity in Flux. The library should automatically detect whether the workload identity is single-tenant or multi-tenant by checking if a `ServiceAccount` was configured for the operation. If a `ServiceAccount` was configured, then the operation is multi-tenant, otherwise it is single-tenant and the granted access token must represent the identity associated with the controller. The directory structure would look like this: ```shell . └── auth ├── aws │ └── aws.go ├── azure │ └── azure.go ├── gcp │ └── gcp.go ├── access_token.go ├── options.go ├── provider.go ├── registry.go ├── restconfig.go └── token.go ``` The file `auth/token.go` would contain the token abstraction: ```go package auth // Token is an interface that represents an access token that can be used to // authenticate with a cloud provider. The only common method is for getting the // duration of the token, because different providers have different ways of // representing the token. For example, Azure and GCP use a single string, // while AWS uses three strings: access key ID, secret access key and token. // Consumers of this interface should know what type to cast it to. type Token interface { // GetDuration returns the duration for which the token is valid relative to // approximately time.Now(). This is used to determine when the token should // be refreshed. GetDuration() time.Duration } ``` The file `auth/access_token.go` would contain the main algorithm for getting access tokens: ```go package auth // GetAccessToken returns an access token for accessing resources in the given cloud provider. func GetAccessToken(ctx context.Context, provider Provider, opts ...Option) (Token, error) { // 1. Check if a ServiceAccount is configured and return the controller access token if not (single-tenant WI). // 2. Get the provider audience for creating the OIDC token for the ServiceAccount in the Kubernetes API. // 3. Get the ServiceAccount using the configured controller-runtime client. // 4. Get the provider identity from the ServiceAccount annotations and add it to the options. // 5. Build the cache key using the configured options. // 6. Get the token from the cache. If present, return it, otherwise continue. // 7. Create an OIDC token for the ServiceAccount in the Kubernetes API using the provider audience. // 8. Exchange the OIDC token for an access token through the Security Token Service of the provider. // 9. Add the final token to the cache and return it. } ``` The file `auth/registry.go` would contain the logic for creating artifact registry credentials: ```go package auth // ArtifactRegistryCredentials is a particular type implementing the Token interface // for credentials that can be used to authenticate against an artifact registry // from a cloud provider. type ArtifactRegistryCredentials struct { authn.Authenticator ExpiresAt time.Time } func (r *ArtifactRegistryCredentials) GetDuration() time.Duration { return time.Until(r.ExpiresAt) } // GetArtifactRegistryCredentials retrieves the registry credentials for the // specified artifact repository and provider. func GetArtifactRegistryCredentials(ctx context.Context, provider Provider, artifactRepository string, opts ...Option) (*ArtifactRegistryCredentials, error) ``` The file `auth/restconfig.go` would contain the logic for creating a REST config for the Kubernetes API: ```go package auth // RESTConfig is a particular type implementing the Token interface // for Kubernetes REST configurations. type RESTConfig struct { Host string BearerToken string CAData []byte ExpiresAt time.Time } // GetDuration implements Token. func (r *RESTConfig) GetDuration() time.Duration { return time.Until(r.ExpiresAt) } // GetRESTConfig retrieves the authentication and connection // details to a remote Kubernetes cluster for the given provider, // cluster resource name and API server address. func GetRESTConfig(ctx context.Context, provider Provider, cluster, address string, opts ...Option) (*RESTConfig, error) ``` The file `auth/provider.go` would contain the `Provider` interface: ```go package auth // Provider contains the logic to retrieve security credentials // for accessing resources in a cloud provider. type Provider interface { // GetName returns the name of the provider. GetName() string // NewControllerToken returns a token that can be used to authenticate // with the cloud provider retrieved from the default source, i.e. from // the environment of the controller pod, e.g. files mounted in the pod, // environment variables, local metadata services, etc. NewControllerToken(ctx context.Context, opts ...Option) (Token, error) // GetAudience returns the audience the OIDC tokens issued representing // ServiceAccounts should have. This is usually a string that represents // the cloud provider's STS service, or some entity in the provider for // which the OIDC tokens are targeted to. GetAudience(ctx context.Context, serviceAccount corev1.ServiceAccount) (string, error) // GetIdentity takes a ServiceAccount and returns the identity which the // ServiceAccount wants to impersonate, by looking at annotations. GetIdentity(serviceAccount corev1.ServiceAccount) (string, error) // NewToken takes a ServiceAccount and its OIDC token and returns a token // that can be used to authenticate with the cloud provider. The OIDC token is // the JWT token that was issued for the ServiceAccount by the Kubernetes API. // The implementation should exchange this token for a cloud provider access // token through the provider's STS service. NewTokenForServiceAccount(ctx context.Context, oidcToken string, serviceAccount corev1.ServiceAccount, opts ...Option) (Token, error) // GetAccessTokenOptionsForArtifactRepository returns the options that must be // passed to the provider to retrieve access tokens for an artifact repository. GetAccessTokenOptionsForArtifactRepository(artifactRepository string) ([]Option, error) // ParseArtifactRepository parses the artifact repository to verify // it's a valid repository for the provider. As a result, it returns // the input required for the provider to issue registry credentials. // This input is included in the cache key for the issued credentials. ParseArtifactRepository(artifactRepository string) (string, error) // NewArtifactRegistryCredentials takes the registry input extracted by // ParseArtifactRepository() and an access token and returns credentials // that can be used to authenticate with the registry. NewArtifactRegistryCredentials(ctx context.Context, registryInput string, accessToken Token, opts ...Option) (*ArtifactRegistryCredentials, error) // GetAccessTokenOptionsForCluster returns the options that must be // passed to the provider to retrieve access tokens for a cluster. // More than one access token may be required depending on the // provider, with different options (e.g. scope). Hence the return // type is a slice of slices. GetAccessTokenOptionsForCluster(cluster string) ([][]Option, error) // NewRESTConfig returns a RESTConfig that can be used to authenticate // with the Kubernetes API server. The access tokens are used for looking // up connection details like the API server address and CA certificate // data, and for accessing the cluster API server itself via the IAM // system of the cloud provider. If it's just a single token or multiple, // it depends on the provider. NewRESTConfig(ctx context.Context, accessTokens []Token, opts ...Option) (*RESTConfig, error) } ``` The file `auth/options.go` would contain the following options: ```go package auth // Options contains options for configuring the behavior of the provider methods. // Not all providers/methods support all options. type Options struct { Client client.Client Cache *cache.TokenCache ServiceAccount *client.ObjectKey InvolvedObject cache.InvolvedObject Audiences []string Scopes []string STSRegion string STSEndpoint string ProxyURL *url.URL ClusterResource string ClusterAddress string CAData string AllowShellOut bool } // WithServiceAccount sets the ServiceAccount reference for the token // and a controller-runtime client to fetch the ServiceAccount and // create an OIDC token for it in the Kubernetes API. func WithServiceAccount(saRef client.ObjectKey, client client.Client) Option { // ... } // WithCache sets the token cache and the involved object for recording events. func WithCache(cache cache.TokenCache, involvedObject cache.InvolvedObject) Option { // ... } // WithAudiences sets the audiences for the Kubernetes ServiceAccount token. func WithAudiences(audiences ...string) Option { // ... } // WithScopes sets the scopes for the token. func WithScopes(scopes ...string) Option { // ... } // WithSTSRegion sets the region for the STS service (some cloud providers // require a region, e.g. AWS). func WithSTSRegion(stsRegion string) Option { // ... } // WithSTSEndpoint sets the endpoint for the STS service. func WithSTSEndpoint(stsEndpoint string) Option { // ... } // WithProxyURL sets a *url.URL for an HTTP/S proxy for acquiring the token. func WithProxyURL(proxyURL url.URL) Option { // ... } // WithCAData sets the CA data for credentials that require a CA, // e.g. for Kubernetes REST config. func WithCAData(caData string) Option { // ... } // WithClusterResource sets the cluster resource for creating a REST config. // Must be the fully qualified name of the cluster resource in the cloud // provider API. func WithClusterResource(clusterResource string) Option { // ... } // WithClusterAddress sets the cluster address for creating a REST config. // This address is used to select the correct cluster endpoint and CA data // when the provider has a list of endpoints to choose from, or to simply // validate the address against the cluster resource when the provider // returns a single endpoint. This is optional, providers returning a list // of endpoints will select the first one if no address is provided. func WithClusterAddress(clusterAddress string) Option { // ... } // WithAllowShellOut allows the provider to shell out to binary tools // for acquiring controller tokens. MUST be used only by the Flux CLI, // i.e. in the github.com/fluxcd/flux2 Git repository. func WithAllowShellOut() Option { // ... } ``` The `auth/aws/aws.go`, `auth/azure/azure.go` and `auth/gcp/gcp.go` files would contain the implementations for the respective cloud providers: ```go package aws import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/sts/types" ) const ProviderName = "aws" type Provider struct{} type Token struct{ types.Credentials } // GetDuration implements auth.Token. func (t *Token) GetDuration() time.Duration { return time.Until(*t.Expiration) } type credentialsProvider struct { opts []auth.Option } // NewCredentialsProvider creates an aws.CredentialsProvider for the aws provider. func NewCredentialsProvider(opts ...auth.Option) aws.CredentialsProvider { return &credentialsProvider{opts} } // Retrieve implements aws.CredentialsProvider. func (c *credentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { // Use auth.GetToken() to get the token. } ``` ```go package azure import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" ) const ProviderName = "azure" type Provider struct{} type Token struct{ azcore.AccessToken } // GetDuration implements auth.Token. func (t *Token) GetDuration() time.Duration { return time.Until(t.ExpiresOn) } type tokenCredential struct { opts []auth.Option } // NewTokenCredential creates an azcore.TokenCredential for the azure provider. func NewTokenCredential(opts ...auth.Option) azcore.TokenCredential { return &tokenCredential{opts} } // GetToken implements azcore.TokenCredential. // The options argument is ignored, any options should be // specified in the constructor. func (t *tokenCredential) GetToken(ctx context.Context, _ policy.TokenRequestOptions) (azcore.AccessToken, error) { // Use auth.GetToken() to get the token. } ``` ```go package gcp import ( "golang.org/x/oauth2" ) const ProviderName = "gcp" type Provider struct {} type Token struct{ oauth2.Token } // GetDuration implements auth.Token. func (t *Token) GetDuration() time.Duration { return time.Until(t.Expiry) } type tokenSource struct { ctx context.Context opts []auth.Option } // NewTokenSource creates an oauth2.TokenSource for the gcp provider. func NewTokenSource(ctx context.Context, opts ...auth.Option) oauth2.TokenSource { return &tokenSource{ctx, opts} } // Token implements oauth2.TokenSource. func (t *tokenSource) Token() (*oauth2.Token, error) { // Use auth.GetToken() to get the token. } var gkeMetadata struct { projectID string location string name string mu sync.Mutex loaded bool } ``` As detailed above, each cloud provider implementation defines a simple wrapper around the cloud provider access token type. This wrapper implements the `auth.Token` interface, which is essentially the method `GetDuration()` for the cache library to manage the token lifetime. The wrappers also contain a helper function to create a token source for the respective cloud provider SDKs. These methods have different names and signatures because the cloud provider SDKs are different and have different types, but they all implement the same concept of a token source. The `aws` provider needs to read the environment variable `AWS_REGION` for configuring the STS client. Even though a specific STS endpoint may be configured, the AWS SDKs require the region to be set regardless. This variable is usually set automatically in EKS pods, and can be manually set by users otherwise (e.g. in Fargate pods). An important detail to take into account in the `azure` provider implementation is using our custom implementation of `azidentity.NewDefaultAzureCredential()` found in kustomize-controller for SOPS decryption. This custom implementation avoids shelling out to the Azure CLI, which is something we strive to avoid in the Flux codebase. This is important because today we are doing this in a few APIs but not others, so it will be a significant improvement to implement this in a single place and use it everywhere. The `gcp` provider needs to load the cluster metadata from the `gke-metadata-server` in order to create tokens. This must be done lazily when the first token is requested, and there's a very important reason for this: if this was done on the controller startup, the controller would crash when running outside GKE and enter `CrashLoopBackOff` because the `gke-metadata-server` would never be available. This is a very important detail that must be taken into account when implementing the `gcp` provider. The cluster metadata doesn't change during the lifetime of the controller pod, so we use a `sync.Mutex` and `bool` to load it only once into a package variable. When not running in GKE, the `gcp` provider would use the following annotation in the `ServiceAccount` to identify the Workload Identity Provider resource for use with Workload Identity Federation: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: my-service-account namespace: my-namespace annotations: gcp.auth.fluxcd.io/workload-identity-provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID ``` #### Cache Key The cache key *MUST* include *ALL* the inputs specified for acquiring the temporary credentials, as they all obviously influence how the credentials are created. ##### Format The cache key would be the SHA256 hash of the following multi-line strings: Single-tenant/controller-level access token cache key: ``` provider= scopes= stsRegion= stsEndpoint= proxyURL= caData= ``` Multi-tenant/object-level access token cache key: ``` provider= providerIdentity= serviceAccountName= serviceAccountNamespace= serviceAccountTokenAudiences= scopes= stsRegion= stsEndpoint= proxyURL= caData= ``` Artifact registry credentials: ``` accessTokenCacheKey=sha256() artifactRepositoryCacheKey=<'gcp'-for-gcp|registry-region-for-aws|registry-host-for-azure> ``` REST config: ``` accessToken1CacheKey=sha256() ... accessTokenNCacheKey=sha256() cluster= address= ``` ##### Security Considerations and Controls As mentioned previously, a `ServiceAccount` must have permission to impersonate the identity it is configured to impersonate. Once a token for the impersonated identity is issued, that token would be valid for a while even if immediately after issuing it the `ServiceAccount` loses permission to impersonate that identity. In our cache key design, the token would remain available for the `ServiceAccount` to use until it expires. If the impersonation permission was revoked to mitigate an attack, the attacker could still get a valid token from the cache for a while after the revocation, and hence still exercise the permissions they had prior to the revocation. There are a few mitigations for this scenario: * Users that revoke impersonation permissions for a `ServiceAccount` must also change the annotations of the `ServiceAccount` to impersonate a different identity, or delete the `ServiceAccount` altogether, or restart the Flux controllers so the cache is purged. Any of these actions would effectively prevent the attack, but they represent an additional step after revoking the impersonation permission. * In the Flux controllers users can specify the `--token-cache-max-duration` flag, which can be used to limit the maximum duration for which a token can be cached. By reducing the default maximum duration of one hour to a smaller value, users can limit the time window during which a token would be available for a `ServiceAccount` to use after losing permission to impersonate the identity. * Disable cache entirely by setting the flag `--token-cache-max-size=0`, or removing this flag altogether since the default is already zero i.e. no tokens are cached in the Flux controller. This mitigation is in case your security requirements are extreme and you want to avoid any risk of such an attack. This mitigation is the most effective, but it comes with the cost of many API calls to issue tokens in the cloud provider, which could result in a performance bottleneck and/or throttling/rate-limiting, as tokens would have to be issued for every reconciliation. A similar situation could occur in the single-tenant scenario, when the permission to impersonate the configured identity is revoked from the controller `ServiceAccount`. In this case, the attacker would have access to the cloud provider resources that the controller had access to prior to the revocation of the impersonation permission. Most of the mitigations mentioned above apply to this scenario as well, except for the one that involves changing the annotations of the `ServiceAccount` to impersonate a different identity or deleting the `ServiceAccount` altogether, as the controller `ServiceAccount` should not be deleted. The best mitigation in this case is to restart the Flux controllers so the cache is purged. ### Library Integration When reconciling an object, the controller must use the `auth.GetToken()` function passing a `controller-runtime` client that has permission to create `ServiceAccount` tokens in the Kubernetes API, the desired cloud provider by name, and all the remaining options according to the configuration of the controller and of the object. The provider names match the ones used for `spec.provider` in the Flux APIs, i.e. `aws`, `azure` and `gcp`. Because different cloud providers have different ways of representing their access tokens (e.g. Azure and GCP tokens are a single opaque string while AWS has three strings: access key ID, secret access key and token), consumers of the `auth.Token` interface would need to cast it to `*.Token`. The following subsections show details of how the integration would look like. #### `GitRepository` and `ImageUpdateAutomation` APIs For these APIs the only provider we have so far that supports workload identity is `azure`. In this case we would simply replace `AzureOpts []azure.OptFunc` in the `fluxcd/pkg/git.ProviderOptions` struct with `[]fluxcd/pkg/auth.Option` and would modify `fluxcd/pkg/git.GetCredentials()` to use `auth.GetToken()`. The token interface would be cast to `*azure.Token` and the token string would be assigned to `fluxcd/pkg/git.Credentials.BearerToken`. A `GitRepository` object configured with the `azure` provider and a `ServiceAccount` would then go through this code path. #### `OCIRepository`, `ImageRepository`, `ImagePolicy`, `HelmRepository` and `HelmChart` APIs The `HelmRepository` API only supports a cloud provider for OCI repositories, so for all these APIs we would only need to support OCI authentication. All these APIs currently use `*fluxcd/pkg/oci/auth/login.Manager` to get the container registry credentials. The new library would replace this library entirely, as it mostly handles single-tenant workload identity. The new library covers both single-tenant and multi-tenant workload identity, so it would be a drop-in replacement for the `login.Manager`. In the case of the source-controller APIs, all of them use the function `OIDCAuth()` from the internal package `internal/oci`. We would replace the use of `login.Manager` with `auth.GetToken()` in this function. The token interface would be cast to `*auth.RegistryCredentials` and then fed to `authn.FromConfig()` from the package `github.com/google/go-containerregistry/pkg/authn`. In the case of `ImageRepository` and `ImagePolicy`, we would replace `login.Manager` with `auth.GetToken()` in the `setAuthOptions()` method of the `ImageRepositoryReconciler`, cast the token to `*auth.RegistryCredentials` and then feed it to `authn.FromConfig()`. The beauty of this particular integration is that here we no longer require branching code paths for each cloud provider, we would just need to configure the options for the `auth.GetToken()` function and the library would take care of the rest. #### `Bucket` API ##### Provider `aws` A `Bucket` object configured with the `aws` provider and a `ServiceAccount` would cause the internal `minio.MinioClient` of source-controller to be created with the following new options: * `minio.WithTokenClient(controller-runtime/pkg/client.Client)` * `minio.WithTokenCache(*fluxcd/pkg/cache.TokenCache)` The constructor would then use `auth.GetToken()` to get the cloud provider access token. When doing so, the `minio.MinioClient` would cast the token interface to `*aws.Token` and feed it to `credentials.NewStatic()` from the package `github.com/minio/minio-go/v7/pkg/credentials`. ##### Provider `azure` A `Bucket` object configured with the `azure` provider and a `ServiceAccount` would cause the internal `azure.BlobClient` of source-controller to be created with the following new options: * `azure.WithTokenClient(controller-runtime/pkg/client.Client)` * `azure.WithTokenCache(*fluxcd/pkg/cache.TokenCache)` * `azure.WithServiceAccount(controller-runtime/pkg/client.ObjectKey)` * `azure.WithInvolvedObject(*fluxcd/pkg/cache.InvolvedObject)` The constructor would then use `azure.NewTokenCredential()` to feed this token credential to `azblob.NewClient()`. ##### Provider `gcp` A `Bucket` object configured with the `gcp` provider and a `ServiceAccount` would cause the internal `gcp.GCSClient` of source-controller to be created with the following new options: * `gcp.WithTokenClient(controller-runtime/pkg/client.Client)` * `gcp.WithTokenCache(*fluxcd/pkg/cache.TokenCache)` * `gcp.WithServiceAccount(controller-runtime/pkg/client.ObjectKey)` * `gcp.WithInvolvedObject(*fluxcd/pkg/cache.InvolvedObject)` The constructor would then use `gcp.NewTokenSource()` to feed this token source to the `option.WithTokenSource()` and pass it to `cloud.google.com/go/storage.NewClient()`. #### `Kustomization` API (SOPS Decryption) The `Kustomization` API uses Key Management Services (KMS) for decrypting SOPS secrets. The internal packages `internal/decryptor` and `internal/sops` of kustomize-controller already use interfaces compatible with the new library in the case of `aws` and `azure`, i.e. `*awskms.CredentialsProvider` and `*azkv.TokenCredential` respectively, so we could easily use the helper functions for creating the respective token sources to configure the KMS credentials for SOPS. This is thanks to the respective SOPS libraries `github.com/getsops/sops/v3/kms` and `github.com/getsops/sops/v3/azkv`. For GCP we can introduce the equivalent interface that was recently added in [this](https://github.com/getsops/sops/pull/1794/files) pull request. This new interface introduced in SOPS upstream can also be used for the current JSON credentials method that we use via `google.CredentialsFromJSON().TokenSource`. This would allow us to use only the respective token source interfaces for all three providers when using either workload identity or secrets. #### `Kustomization` and `HelmRelease` APIs (Remote Cluster Access) The kustomize-controller should fetch a `*rest.Config` from the `auth` package and feed it to `runtime/client.WithKubeConfig()` for creating a `runtime/client.(*Impersonator)` with the configured authentication. The helm-controller should fetch a `*rest.Config` from the `auth` package and feed it to the internal `kube.NewMemoryRESTClientGetter()`, just like it does for the secret-based alternative. #### `Provider` API The constructor of the internal `notifier.Factory` of notification-controller would now accept the following new options: * `notifier.WithTokenClient(controller-runtime/pkg/client.Client)` * `notifier.WithTokenCache(*fluxcd/pkg/cache.TokenCache)` * `notifier.WithServiceAccount(controller-runtime/pkg/client.ObjectKey)` * `notifier.WithInvolvedObject(*fluxcd/pkg/cache.InvolvedObject)` The cloud provider types that support workload identity would then use these options. See the following subsections for details. ##### Type `azuredevops` The `notifier.NewAzureDevOps()` constructor would use the existing and new options to call `auth.GetToken()` and use it to get the cloud provider access token. When doing so, the `notifier.AzureDevOps` would cast the token interface to `*azure.Token` and feed the token string to `NewPatConnection()` from the package `github.com/microsoft/azure-devops-go-api/azuredevops/v6`. ##### Type `azureeventhub` The `notifier.NewAzureEventHub()` constructor would use the existing and new options to call `auth.GetToken()` and use it to get the cloud provider access token. When doing so, the `notifier.AzureEventHub` would cast the token interface to `*azure.Token` and feed the token string to `newJWTHub()`. ##### Type `googlepubsub` The `notifier.NewGooglePubSub()` constructor would use the existing and new options to call `gcp.NewTokenSource()` and feed this token source to the `option.WithTokenSource()` and pass it to `cloud.google.com/go/pubsub.NewClient()`. ## Implementation History * In Flux 2.6 object-level workload identity was introduced for the OCI artifact APIs, i.e. `OCIRepository`, `ImageRepository`, `ImagePolicy`, `HelmRepository` and `HelmChart`, as well as for SOPS decryption in the `Kustomization` API and Azure Event Hubs in the `Provider` API. * In Flux 2.7 object-level workload identity was introduced for all the remaining APIs that support cloud providers, i.e. `Bucket`, `GitRepository` and `ImageUpdateAutomation`, and also all the remaining types for the `Provider` API, i.e. `azuredevops` and `googlepubsub`. In addition, support for controller and object-level workload identity was introduced for the `Kustomization` and `HelmRelease` APIs for remote cluster access. ================================================ FILE: rfcs/0011-opentelemetry-tracing/README.md ================================================ # RFC-0011: OpenTelemetry Tracing **Status:** implemented **Creation date:** 2025-04-24 **Last update:** 2026-03-13 ## Summary The aim is to be able to collect traces via OpenTelemetry (OTel) across all Flux related objects, such as HelmReleases, Kustomizations and among others. These may be sent towards a tracing provider where may be potentially stored and visualized. Flux does not have any responsibility on storing and visualizing those, it keeps being completely stateless. Thereby, being seamless for the user, the implementation is going to be part of the already existing `Alert` API Type. Therefore, `EventSources` is going to discriminate the events belonging to the specific sources, which are going to be looked up to and send them out towards the `Provider` set. In this way, it could facilitate the observability and monitoring of Flux related objects. ## Motivation This RFC was born out of a need for end-to-end visibility into Flux’s multi-controller GitOps workflow. At the time Flux was one monolithic controller; it has since split into several specialized controllers (source-, kustomize-, helm-, notification-, etc.), which makes tracing the path of a single "Source change → applied resource → notification” much harder. Additionally, users may not have to implement tools/sidecars around to maintain. Correlate any potential source (GitRepository, OCIRepository, HelmChart or Bucket) with all downstream actions. Therefore, you would like to see a single trace (with multiple spans underneath): - Alert reference based on a unique ID (root trace). - Any source pulling new content based on a new Digest Checksum. - Any subsequent reconciliation that ran. - Events emitted and notifications sent by the notification-controller. On top of this, can be built custom UIs that surface trace timelines alongside Git commit or Docker image tags, so operators can say “what exactly happened when I tagged v1.2.3?” in a single pane of glass. ### Goals - **End-to-end GitOps traceability:** Capture the traces that follows "a Git change" (any source) through all Flux controllers for simply debugging and root-cause analysis. - **Declarative, CRD-driven configuration:** Reuse the concept of `Alerts` to be able to populate this feature over, out-of-the-box. Therefore, users can link `EventSources` and `Provider` where trace will be sent. - **Notification Controller as the trace-collector:** Leverage the notification-controller's existing event watching pipeline to ingest reconciliation events and turn them into OpenTelemetry spans, being forwarded to an OpenTelemetry-compatible backend - `Provider`. - **Cross-controller span correlation:** Ensure spans are emitted from multiple, stateless controller can be stitched together into a single trace by using Flux "revision" annotation. ### Non-Goals - **Not a full-tracing backend:** We won't build or bundle a storage/visualization system. Users may have to still rely on a external collector for long-term retention, querying and UI. - **Not automatic instrumentation of user workloads:** This integration only captures Flux controller events (Source, Kustomize, Helm, etc.). It won't auto-inject spans into your application pods or third-party controllers running in the same cluster. - **Not a replacement for metrics or logs:** Flux's existing Prometheus metrics and structural logging remain the primary way to monitor performance and errors. Tracing is purely for request-flow visibility, not for time-series monitoring or log aggregation. - **No deep-code level spans beyond CRUD events:** Will emit spans around high-level reconciliation steps (e.g. "reconcile GitRepository", "dispatch Notification"), but we're not aiming to instrument every internal function call or library method within each controller. - **Not a service mesh integration:** It's not plan of the scope tying this into Istio, Linkerd, or other mesh-sidecar approaches. It's strictly a controller-drive, CRD-based model. - **No per-span custom enrichment beyond basic metadata:** At least initially, it won't support complex span attributes or tag-enrichment rules. You may have to handle those in your downstream collector/processor if needed. - **Not a replacement for user-driven OpenTelemetry SDKs:** If you already have a Go-based operator that embed OpenTelemetry's SDK directly, this feature won't override or duplicate that. Think about it as a complementary, declarative layer for flux controllers. ## Proposal The implementation will extend the notification-controller with OpenTelemetry tracing capabilities by leveraging the existing Alert API object model and adding a new Provider API type called `otel`. This approach maintains Flux's declarative configuration paradigm while adding powerful distributed tracing functionality. ### Core Implementation Strategy 1. **Extend the notification-controller:** Add OpenTelemetry tracing support to the notification-controller via adding a new type, `otel`. Which already has visibility into events across the Flux ecosystem. 2. **Leverage existing Alert CRD structure:** Use the Alert Kind API object as the configuration entry point, where: - `EventSources` define which Flux resources to trace (GitRepositories, Kustomizations, HelmReleases, etc.). - `Provider` specifies where to send the trace data (any OpenTelemetry-compatible backends). 3. **Span generation and correlation:** Generate spans for each reconciliation event from watched resources, ensuring proper parent-child relationships and context propagation using Flux's revision annotations as correlation identifiers. 4. **Provider compatibility and fallback mechanism:** The implementation supports any provider that implements the OpenTelemetry Protocol (OTLP). When traces are sent to OTLP-compatible providers (like Jaeger or Tempo), they are transmitted as proper OpenTelemetry spans via HTTP(s) requests (no gRPC support at this moment). For non-OTLP providers, the system gracefully degrades by logging trace information as structured warnings in the notification-controller logs, ensuring no alerting functionality is disrupted. This approach maintains system stability while encouraging the use of proper tracing backends. This approach allows users to declaratively configure tracing using familiar Flux patterns, without requiring code changes to their applications or additional sidecar deployments. The notification-controller will handle the collection, correlation, and forwarding of spans to the configured tracing backend. Example Configuration: ```yaml # Configure the alert apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: webapp-tracing namespace: default spec: providerRef: name: otel-collector eventSources: - kind: GitRepository # Source controller resources name: webapp-source - kind: Kustomization # Kustomize controller resources name: webapp-backend - kind: Kustomization # Kustomize controller resources name: webapp-frontend eventMetadata: env: staging cluster: cluster-1 region: us-east-2 --- # Define a tracing provider apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: otel-collector namespace: default spec: type: otel address: http://otel-collector.observability.svc.cluster.local:4318/v1/traces # OTEL Collector endpoint secretRef: name: otel-collector-secret # Optional: auth + additional headers certSecretRef: name: mtls-certs # Optional: enable mTLS auth proxySecretRef: name: otel-collector-proxy # Optional: proxy configuration --- # OTEL Collector secret apiVersion: v1 kind: Secret metadata: name: otel-collector-secret namespace: default stringData: # Headers data prevails over auth fields (username/password or token) # Must be used either username/password or token (considers if username is set in order to discriminate bearer token auth or basic auth) username: "" password: "" token: "" headers: | X-Forwarded-Proto: https --- # TLS Certificates and keys apiVersion: v1 kind: Secret metadata: name: mtls-certs namespace: default type: kubernetes.io/tls # or Opaque stringData: # All fields are required to enable mTLS tls.crt: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- tls.key: | -----BEGIN PRIVATE KEY----- -----END PRIVATE KEY----- # Just ca.crt in case of CA-only ca.crt: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- --- # Proxy configuration apiVersion: v1 kind: Secret metadata: name: otel-collector-proxy namespace: default stringData: address: "http://(:)" username: "" password: "" ``` Based on this configuration, the notification-controller will: - Watch for events from the specified resources. - Generate OpenTelemetry spans for each reconciliation event. - Correlate spans across controllers using Flux's revision annotations. - Forward the spans to the configured OTEL Collector endpoint - `Provider`. - This implementation maintains Flux's stateless design principles while providing powerful distributed tracing capabilities that help users understand and troubleshoot their GitOps workflows. ### Alternatives ## Design Details ### Trace Identity and Correlation A key challenge in distributed tracing is establishing a reliable correlation mechanism that works across multiple controllers in a stateless, potentially unreliable environment. Our solution addresses this with a robust span identification strategy. The Trace ID is generated using a deterministic approach that combines: - **Alert Object UID** (guaranteed unique by Kubernetes across all clusters). - **Source's revision ID** (extracted from event payloads). These values are concatenated and passed through a configurable checksum algorithm (SHA-256 by default). This approach ensures: - Globally unique trace identifiers across multi-tenant and multi-cluster environments. - Consistent trace correlation even when events arrive out of order. - Reliable identification of the originating source event. Example: ```yaml # Input values Alert UID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" Source Revision: "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" # Concatenated value "a1b2c3d4-e5f6-7890-abcd-ef1234567890():sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae()" # Apply SHA-256 (default algorithm) Trace ID: "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650" ``` When events occur in the system: 1. GitRepository reconciliation event with revision "sha256:2c26..." is captured by notification controller and creates a new trace with ID "f7846f55..." and therefore, a new Span underneath. 2. Kustomization acts on the previous one, creating another span, reusing the same trace ID and then linked to "f7846f55...". Being both under the same trace. 3. All spans are collected into a single trace viewable in the tracing backend. ### Resilient Span Management The design accounts for the distributed nature of Flux controllers and potential delays/downtimes that a distributed system always implies: - **Asynchronous Event Processing:** Since events may arrive in any order due to the distributed nature of Flux controllers, the system doesn't assume sequential processing. Each event can independently locate its parent span or create a new root span as needed. - **Fault Tolerance:** If the notification-controller experiences downtime or latency issues, it implements a recovery mechanism: - When processing an event, it first attempts to locate an existing root span based on the calculated ID. - If found, it attaches the new span as a child to maintain the trace hierarchy. - If not found (due to previous failures or out-of-order processing), it automatically creates a new root span - Span Hierarchy Maintenance: All subsequent spans related to the same revision are properly attached to their parent spans, creating a coherent trace visualization regardless of when events are processed. This design ensures trace continuity even in challenging distributed environments while maintaining Flux's core principles of statelessness and resilience. ## Implementation History * RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0) ================================================ FILE: rfcs/0012-external-artifact/README.md ================================================ # RFC-0012 External Artifact **Status:** implemented **Creation date:** 2025-04-08 **Last update:** 2026-03-13 ## Summary This RFC proposes the introduction of a new API called `ExternalArtifact` that would allow 3rd party controllers to act as a source of truth for the cluster desired state. In effect, the `ExternalArtifact` API acts as an extension of the existing `source.toolkit.fluxcd.io` APIs that enables Flux `kustomize-controller` and `helm-controller` to consume artifacts from external source types that are not natively supported by `source-controller`. ## Motivation Over the years, we've received requests from users to support other source types besides the ones natively supported by `source-controller`. For example, users have asked for support of downloading Kubernetes manifests from GitHub/GitLab releases, Omaha protocol, SFTP protocol, and other remote storage systems. Another common request is to run transformations on the artifacts fetched by source-controller. For example, users want to be able to generate YAML manifests from jsonnet, cue, and other templating engines before they are consumed by Flux `kustomize-controller`. In order to support these use cases, we need to define a standard API that allows 3rd party controllers to expose artifacts in-cluster (in the same way `source-controller` does) that can be consumed by Flux `kustomize-controller` and `helm-controller`. ### Goals Define a standard API for 3rd party controllers to expose artifacts that can be consumed by Flux controllers in the same way as the existing `source.toolkit.fluxcd.io` APIs. Allow Flux users to transition from using `source-controller` to using 3rd party source controllers with minimal changes to their existing `Kustomizations` and `HelmReleases`. ### Non-Goals Allow arbitrary custom resources to be referenced in Flux `Kustomization` and `HelmRelease` as `sourceRef`. Extend the Flux controllers permissions to access custom resources that are not part of the `source.toolkit.fluxcd.io` APIs. ## Proposal Assuming we have a custom controller called `release-controller` that is responsible for reconciling `GitHubRelease` custom resources. This controller downloads the Kubernetes deployment YAML manifests from the GitHub API and stores them in a local file system as a `tar.gz` file. The `release-controller` then creates an `ExternalArtifact` custom resource that tells the Flux controllers from where to fetch the artifact. Every time the `release-controller` reconciles a `GitHubRelease` custom resource, it updates the `ExternalArtifact` status with the latest artifact information if the upstream release has changed. The `release-controller` is responsible for exposing a HTTP endpoint that serves the artifacts from its own storage. The URL of the `tar.gz` artifact is stored in the `ExternalArtifact` status and should be accessible from the Flux controllers running in the cluster. Example of a generated `ExternalArtifact` custom resource: ```yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: ExternalArtifact metadata: name: podinfo namespace: apps spec: # SourceRef points to the Kubernetes custom resource for # which the artifact is generated. # +optional sourceRef: apiVersion: source.example.com/v1alpha1 kind: GitHubRelease name: podinfo namespace: apps status: artifact: # Digest is the digest of the tar.gz file in the form of ':'. # The digest is used by the Flux controllers to verify the integrity of the artifact. # +required digest: sha256:35d47c9db0eee6ffe08a404dfb416bee31b2b79eabc3f2eb26749163ce487f52 # LastUpdateTime is the timestamp corresponding to the last update of the # Artifact in storage. # +required lastUpdateTime: "2025-03-21T13:37:31Z" # Path is the relative file path of the Artifact. It can be used to locate # the file in the root of the Artifact storage on the local file system of # the controller managing the Source. # +required path: release/apps/podinfo/6.8.0-b3396ad.tar.gz # Revision is a human-readable identifier traceable in the origin source system # in the form of '@:'. # The revision is used by the Flux controllers to determine if the artifact has changed. # +required revision: 6.8.0@sha256:35d47c9db0eee6ffe08a404dfb416bee31b2b79eabc3f2eb26749163ce487f52 # Size is the number of bytes of the tar.gz file. # +required size: 20914 # URL is the in-cluster HTTP address of the Artifact as exposed by the controller # managing the Source. It can be used to retrieve the Artifact for # consumption, e.g. by kustomize-controller applying the Artifact contents. # +required url: http://release-controller.flux-system.svc.cluster.local./release/apps/podinfo/6.8.0-b3396ad.tar.gz conditions: - lastTransitionTime: "2025-04-08T09:09:49Z" message: stored artifact for release 6.8.0 observedGeneration: 1 reason: Succeeded status: "True" type: Ready ``` Note that the `.status.artifact` is identical to how `source-controller` exposes the artifact information for `Bucket`, `GitRepository`, and `OCIRepository` custom resources. This allows the Flux controllers to consume external artifacts with minimal changes. The `ExternalArtifact` custom resource is referenced by a Flux `Kustomization` as follows: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo namespace: apps spec: interval: 10m sourceRef: kind: ExternalArtifact name: podinfo path: "./" prune: true ``` Flux `kustomize-controller` will then fetch the artifact from the URL specified in the `ExternalArtifact` status, verifies the integrity of the artifact using the digest and applies the contents of the artifact to the cluster. Like with the existing `source.toolkit.fluxcd.io` APIs, `kustomize-controller` will watch the `ExternalArtifact` custom resource for changes and will re-apply the contents of the artifact when the `.status.artifact.revision` changes. When the `ExternalArtifact` contains a Helm chart, it can be referenced by a Flux `HelmRelease` as follows: ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: podinfo namespace: apps spec: interval: 10m releaseName: podinfo chartRef: kind: ExternalArtifact name: podinfo values: replicaCount: 2 ``` ### Security Considerations With the introduction of the `ExternalArtifact` API, the trust boundary of Flux is extended to include 3rd party controllers that are capable of creating and managing `ExternalArtifact` custom resources in the cluster. This means that the security posture of the cluster is now dependent on the security of these 3rd party controllers. To mitigate potential security risks, it is recommended to implement the following measures when developing 3rd party source controllers: - **Authentication and Authorization**: Ensure that the controller uses proper authentication and authorization mechanisms to interact with upstream sources and avoid embedding sensitive information directly in the custom resource specifications. Following source-controller best practices for managing credentials is highly recommended: use `serviceAccountName` to integrate with Kubernetes Workload Identity for short-lived credentials, use `secretRef` to reference long-lived credentials, never cache long-lived credentials on disk or in-memory. - **TLS Encryption**: Use TLS encryption for all communications between the controller and upstream sources to protect sensitive data in transit. Following source-controller best practices for TLS is highly recommended: use `certSecretRef` to reference custom CA certificates and client certificates, prefer Mutual TLS authentication, never allow skipping TLS verification. - **Provenance and Integrity**: Ensure that the controller verifies the integrity of the artifacts it generates and exposes in-cluster. This can be achieved by using checksums and digital signatures to validate the authenticity of upstream sources. Following source-controller best practices for source integrity is highly recommended: verify the provenance of upstream artifacts using Sigstore Cosign or Notary Notation signatures, prefer keyless verification using OIDC identity tokens and public transparency logs. - **Access Control**: Implement access control mechanisms to restrict cross-namespace generation of `ExternalArtifact` custom resources. Following source-controller best practices for access control is highly recommended: expose a `--no-cross-namespace-refs` flag to restrict the controller from generating `ExternalArtifact` resources in a different namespace than the one where the source custom resource is located. Use Kubernetes owner references to establish a clear ownership relationship between the source custom resource and the `ExternalArtifact` resource, allowing Kubernetes garbage collection to clean up the `ExternalArtifact` when the source resource is deleted. - **Least Privilege**: Run the controller with the least privilege necessary to perform its functions. Following source-controller best practices for least privilege is highly recommended: use a dedicated Kubernetes service account with minimal RBAC permissions, avoid running the controller as a cluster-admin or with wildcard permissions, conform with the restricted pod security standard (e.g., disallow running as root, disallow host network access, read-only rootfs). - **Artifact persistent storage integrity**: Ensure that the controller can be configured to use persistent storage for storing artifacts, to avoid data loss in case of controller restarts or failures. Following source-controller best practices for artifact storage is highly recommended: at startup, ensure that the artifacts in-storage have not been tampered with by verifying the checksums of all stored artifacts against the `ExternalArtifact` digests in the cluster. - **Artifact access restrictions**: If the controller is deployed outside of flux-system namespace, it should include network policies that restrict access to the artifact storage endpoint to only kustomize-controller and helm-controller. Following source-controller best practices for network policies is highly recommended: use Kubernetes NetworkPolicies to restrict ingress and egress traffic to/from the controller pods, allowing only necessary communication with upstream sources and trusted consumers. ### User Stories #### 3rd Party Source Controller As a 3rd party controller developer, I want to expose artifacts in-cluster that are sourced from `flatcar/nebraska` that can be consumed by Flux `kustomize-controller` and `helm-controller` so that Flux users can use my controller as a source of truth for their cluster desired state. #### Custom Source Transform As a Flux user, I want to use a custom controller that generates Kubernetes manifests from CUE templates which can be consumed by Flux `kustomize-controller`. #### Policy Enforcement As a cluster administrator, I want to ensure that only trusted 3rd party controllers can create and manage `ExternalArtifact` resources in the cluster. ```yaml apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy metadata: name: "trusted-external-artifacts" spec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: ["source.toolkit.fluxcd.io"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["externalartifacts"] validations: # Restrict the sourceRef to only allow trusted APIs - expression: > object.spec.sourceRef.apiVersion.startsWith('source.example.com') # Restrict the sourceRef to only allow trusted Kinds - expression: > object.spec.sourceRef.kind == 'GitHubRelease' || object.spec.sourceRef.kind == 'GitLabRelease' # Restrict the artifacts to be served only by trusted endpoints within the cluster - expression: > !has(object.status.artifact) || object.status.artifact.url.startsWith('http://release-controller.flux-system.svc.cluster.local./') # Restrict the artifact operations to trusted service accounts - expression: > request.userInfo.username == 'system:serviceaccount:flux-system:release-controller' ``` ### Alternatives An alternative to this proposal would be to deploy an OCI registry in-cluster. The 3rd party controllers would then push the artifacts to the registry and Flux `kustomize-controller` and `helm-controller` would consume the artifacts via the `OCIRepository` custom resource. While this approach is feasible, it requires additional infrastructure and configuration, which may not be desirable for all users. The `ExternalArtifact` API provides a simpler and more flexible way to expose artifacts in-cluster without the need to self-host an OCI registry. In addition, the `ExternalArtifact` API offers a better user experience by allowing Flux user to trace the origin of reconciled resources back to the original source via `ExternalArtifact.spec.sourceRef` and `flux trace` command. ## Design Details The `ExternalArtifact` API will be added to `source-controller/api` Go package. 3rd party controllers will import `github.com/fluxcd/source-controller/api/v1` to generate valid `ExternalArtifact` custom resources using the controller-runtime client. The Flux maintainers will develop an SDK for packaging and exposing artifacts in-cluster using the `ExternalArtifact` API. The SDK will provide helper functions for generating Flux-compliant artifacts, as well as for storing artifacts in a persistent storage. The SDK will be published as a Go module under the `github.com/fluxcd/pkg` repository. The `ExternalArtifact` CRD will be bundled with the `source-controller` CRDs manifests which are part of the standard Flux distribution. This means that users will not need to install the `ExternalArtifact` CRD separately, as it will be available out of the box with Flux. The `ExternalArtifact` API specifications will be published to the Flux documentation website, under the `source.toolkit.fluxcd.io` API reference section. The Flux `Kustomization` and `HelmRelease` APIs will be extended to support the `ExternalArtifact` kind as a valid `sourceRef.kind` and `chartRef.kind`. The `kustomize-controller` and `helm-controller` will gain the ability to consume artifacts from `ExternalArtifact` and watch for revision changes. The `flux trace` command will be extended to support the `ExternalArtifact` API, allowing Flux users to trace any Kubernetes resource in-cluster that originates from an `ExternalArtifact` and see the `sourceRef` information that points to the original source. The `flux` CLI will implement the `flux get externalartifact` command for listing and status checking of `ExternalArtifact` custom resources in the cluster. ### Feature Gate While the `ExternalArtifact` API will be available out of the box with Flux, the ability for `kustomize-controller` and `helm-controller` to consume artifacts from `ExternalArtifact` resources will be behind a feature gate called `ExternalArtifact`. The feature gate will be disabled by default and can be enabled by setting the `--feature-gates=ExternalArtifact=true` flag on the `kustomize-controller` and `helm-controller` deployments. This allows cluster administrators to control the adoption of the `ExternalArtifact` feature in their clusters. ## Implementation History * RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0) ================================================ FILE: rfcs/README.md ================================================ # Flux RFCs In many cases, new features and enhancements are proposed on [flux2/discussions](https://github.com/fluxcd/flux2/discussions). A proposal is discussed in public by maintainers, contributors, users and other interested parties. After some form of consensus is reached between participants, the proposed changes go through the pull request process where the implementation details are reviewed, approved or rejected by maintainers. Some proposals may be **substantial**, and for these we ask for a design process to be followed so that all stakeholders can be confident about the direction Flux is evolving in. The "RFC" (request for comments) process is intended to provide a consistent and controlled path for substantial changes to enter Flux. Examples of substantial changes: - API additions (new kinds of resources, new relationships between existing APIs) - API breaking changes (new required fields, field removals) - Security related changes (Flux controllers permissions, tenant isolation and impersonation) - Impactful UX changes (new required inputs to the bootstrap process) - Drop capabilities (sunset an existing integration with an external service due to security concerns) ## RFC Process - Before submitting an RFC please discuss the proposal with the Flux community. Start a discussion on GitHub and ask for feedback at the weekly dev meeting. You must find a maintainer willing to sponsor the RFC. - Submit an RFC by opening a pull request using [RFC-0000](RFC-0000/README.md) as template. - The sponsor will assign the PR to themselves, will label the PR with `area/RFC` and will request other maintainers to begin the review process. - Integrate feedback by adding commits without overriding the history. - At least two maintainers have to approve the proposal before it can be merged. Approvers must be satisfied that an [appropriate level of consensus](https://github.com/fluxcd/community/blob/main/GOVERNANCE.md#decision-guidelines) has been reached. - Before the merge, an RFC number is assigned by the sponsor and the PR branch must be rebased with main. - Once merged, the proposal may be implemented in Flux. The progress could be tracked using the RFC number (used as prefix for issues and PRs). - After the proposal implementation is available in a release candidate or final release, the RFC should be updated with the Flux version added to the "Implementation History" section. - During the implementation phase, the RFC could be discarded due to security or performance concerns. In this case, the RFC "Implementation History" should state the rejection motives. Ultimately the decision on the feasibility of a particular implementation, resides with the maintainers that reviewed the code changes. - A new RFC could be summited with the scope of replacing an RFC rejected during implementation. The new RFC must come with a solution for the rejection motives of the previous RFC. ================================================ FILE: rfcs/RFC-0000/README.md ================================================ # RFC-NNNN Title **Status:** provisional **Creation date:** YYYY-MM-DD **Last update:** YYYY-MM-DD ## Summary ## Motivation ### Goals ### Non-Goals ## Proposal ### User Stories ### Alternatives ## Design Details ## Implementation History ================================================ FILE: tests/.gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/terraform # Edit at https://www.toptal.com/developers/gitignore?templates=terraform ### Terraform ### # Local .terraform directories **/.terraform/* *.terraform.lock.hcl # test files build/ # .tfstate files *.tfstate *.tfstate.* # Crash log files crash.log # Exclude all .tfvars files, which are likely to contain sentitive data, such as # password, private keys, and other secrets. These should not be part of version # control as they are data points which are potentially sensitive and subject # to change depending on the environment. # *.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in override.tf override.tf.json *_override.tf *_override.tf.json # Include override files you do wish to add to version control using negated pattern # !example_override.tf # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan # example: *tfplan* # Ignore CLI configuration files .terraformrc terraform.rc .env # End of https://www.toptal.com/developers/gitignore/api/terraform ================================================ FILE: tests/bootstrap/main.go ================================================ package main import ( "context" "log" "os" "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitprovider" "k8s.io/client-go/util/retry" ) func main() { ks := "test-cluster/flux-system/kustomization.yaml" patchName := "test-cluster/flux-system/gotk-patches.yaml" ksContent := `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - gotk-components.yaml - gotk-sync.yaml patches: - path: gotk-patches.yaml target: kind: Deployment` patchContent := `apiVersion: apps/v1 kind: Deployment metadata: name: all-flux-components spec: template: metadata: annotations: # Required by Kubernetes node autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict: "true" spec: securityContext: runAsUser: 10000 fsGroup: 1337 containers: - name: manager securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false runAsNonRoot: true capabilities: drop: - ALL ` commitFiles := []gitprovider.CommitFile{ { Path: &ks, Content: &ksContent, }, { Path: &patchName, Content: &patchContent, }, } orgName := os.Getenv("GITHUB_ORG_NAME") repoName := os.Getenv("GITHUB_REPO_NAME") githubToken := os.Getenv(github.TokenVariable) client, err := github.NewClient(gitprovider.WithOAuth2Token(githubToken)) if err != nil { log.Fatalf("error initializing github client: %s", err) } repoRef := gitprovider.OrgRepositoryRef{ OrganizationRef: gitprovider.OrganizationRef{ Organization: orgName, Domain: github.DefaultDomain, }, RepositoryName: repoName, } var repo gitprovider.OrgRepository err = retry.OnError(retry.DefaultRetry, func(err error) bool { return err != nil }, func() error { repo, err = client.OrgRepositories().Get(context.Background(), repoRef) return err }) if err != nil { log.Fatalf("error getting %s repository in org %s: %s", repoRef.RepositoryName, repoRef.Organization, err) } _, err = repo.Commits().Create(context.Background(), "main", "add patch manifest 3", commitFiles) if err != nil { log.Fatalf("error making commit: %s", err) } } ================================================ FILE: tests/image-automation/auto.yaml ================================================ apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageRepository metadata: name: podinfo namespace: flux-system spec: image: ghcr.io/stefanprodan/podinfo interval: 10m --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImagePolicy metadata: name: podinfo namespace: flux-system spec: interval: 10m imageRepositoryRef: name: podinfo policy: semver: range: 6.x digestReflectionPolicy: Always --- apiVersion: image.toolkit.fluxcd.io/v1 kind: ImageUpdateAutomation metadata: name: flux-system namespace: flux-system spec: interval: 5m0s sourceRef: kind: GitRepository name: flux-system git: checkout: ref: branch: main commit: author: email: fluxcdbot@users.noreply.github.com name: fluxcdbot messageTemplate: | Automated image update Automation name: {{ .AutomationObject }} Files: {{ range $filename, $_ := .Changed.FileChanges -}} - {{ $filename }} {{ end -}} Changes: {{ range $resource, $changes := .Changed.Objects -}} {{- range $_, $change := $changes }} - {{ $change.OldValue }} -> {{ $change.NewValue }} {{ end -}} {{ end -}} push: branch: main update: path: ./test-cluster/podinfo-auto strategy: Setters ================================================ FILE: tests/image-automation/kustomization.yaml ================================================ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: flux-system resources: - https://raw.githubusercontent.com/stefanprodan/podinfo/6.8.0/kustomize/deployment.yaml - auto.yaml images: - name: ghcr.io/stefanprodan/podinfo newName: ghcr.io/stefanprodan/podinfo # {"$imagepolicy": "flux-system:podinfo:name"} newTag: 6.8.0 # {"$imagepolicy": "flux-system:podinfo:tag"} digest: "sha256:6c1975b871efb327528c84d46d38e6dd7906eecee6402bc270eeb7f1b1a506df" # {"$imagepolicy": "flux-system:podinfo:digest"} ================================================ FILE: tests/image-automation/main.go ================================================ package main import ( "context" "log" "os" "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitprovider" "k8s.io/client-go/util/retry" ) func main() { ksPath := "test-cluster/podinfo-auto/kustomization.yaml" autoPath := "test-cluster/podinfo-auto/auto.yaml" ksContent, err := os.ReadFile("kustomization.yaml") if err != nil { log.Fatal(err) } ks := string(ksContent) autoContent, err := os.ReadFile("auto.yaml") if err != nil { log.Fatal(err) } auto := string(autoContent) commitFiles := []gitprovider.CommitFile{ { Path: &ksPath, Content: &ks, }, { Path: &autoPath, Content: &auto, }, } orgName := os.Getenv("GITHUB_ORG_NAME") repoName := os.Getenv("GITHUB_REPO_NAME") githubToken := os.Getenv(github.TokenVariable) client, err := github.NewClient(gitprovider.WithOAuth2Token(githubToken)) if err != nil { log.Fatalf("error initializing github client: %s", err) } repoRef := gitprovider.OrgRepositoryRef{ OrganizationRef: gitprovider.OrganizationRef{ Organization: orgName, Domain: github.DefaultDomain, }, RepositoryName: repoName, } var repo gitprovider.OrgRepository err = retry.OnError(retry.DefaultRetry, func(err error) bool { return err != nil }, func() error { repo, err = client.OrgRepositories().Get(context.Background(), repoRef) return err }) if err != nil { log.Fatalf("error getting %s repository in org %s: %s", repoRef.RepositoryName, repoRef.Organization, err) } _, err = repo.Commits().Create(context.Background(), "main", "automation test", commitFiles) if err != nil { log.Fatalf("error making commit: %s", err) } } ================================================ FILE: tests/integration/Makefile ================================================ GO_TEST_ARGS ?= PROVIDER_ARG ?= TEST_TIMEOUT ?= 60m FLUX_BINARY ?= ../../bin/flux test: sops-check mkdir -p build cp $(FLUX_BINARY) build/flux # These two versions of podinfo are pushed to the cloud registry and used in tests for ImageUpdateAutomation docker pull ghcr.io/stefanprodan/podinfo:6.0.0 docker pull ghcr.io/stefanprodan/podinfo:6.0.1 go test -timeout $(TEST_TIMEOUT) -v ./ $(GO_TEST_ARGS) $(PROVIDER_ARG) test-azure: $(MAKE) test PROVIDER_ARG="-provider azure" GO_TEST_ARGS="--tags azure $(GO_TEST_ARGS)" test-gcp: $(MAKE) test PROVIDER_ARG="-provider gcp" destroy: go test -timeout $(TEST_TIMEOUT) -v ./ $(GO_TEST_ARGS) $(PROVIDER_ARG) -destroy-only destroy-azure: $(MAKE) destroy PROVIDER_ARG="-provider azure" destroy-gcp: $(MAKE) destroy PROVIDER_ARG="-provider gcp" sops-check: ifeq ($(shell which sops),) $(error "no sops in PATH, consider installing") endif ================================================ FILE: tests/integration/README.md ================================================ # E2E Tests The goal is to verify that Flux integration with cloud providers are actually working now and in the future. Currently, we only have tests for Azure and GCP. ## General requirements These CLI tools need to be installed for each of the tests to run successfully. - Docker CLI for registry login. - [SOPS CLI](https://github.com/mozilla/sops) for encrypting files - kubectl for applying certain install manifests. ## Azure ### Architecture The [azure](./terraform/azure) Terraform creates the AKS cluster and related resources to run the tests. It creates: - An Azure Container Registry - An Azure Kubernetes Cluster - Two Azure DevOps repositories - Azure EventHub for sending notifications - An Azure Key Vault ### Requirements - Azure account with an active subscription to be able to create AKS and ACR, and permission to assign roles. Role assignment is required for allowing AKS workloads to access ACR. - Azure CLI, need to be logged in using `az login` as a User or as a Service Principal - An Azure DevOps organization, personal access token and ssh keys for accessing repositories within the organization. The scope required for the personal access token is: - `Project and Team` - read, write and manage access - `Code` - Full - Please take a look at the [terraform provider](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/guides/authenticating_using_the_personal_access_token#create-a-personal-access-token) for more explanation. - Azure DevOps only supports RSA keys. Please see [documentation](https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops#set-up-ssh-key-authentication) for how to set up SSH key authentication. - When using in CI, create a test user and use the test user's PAT and SSH key for all Azure DevOps interactions. To grant the test user access in Azure DevOps: - Go to `Organization Settings` on the sidebar of the organization page. - Under `General` > `Users`, click on `Add User` and input the user's email, select `Access Level` of `Basic`. - Go to `Security` > `Permissions`, click on the `User` tab. - For the invited user, set the following permissions to `Allow`: - `General: Create new project`. - The user will get an email invitation and would need to create a Microsoft account if they don't have one yet. **NOTE:** To use Service Principal (for example in CI environment), set the `ARM-*` variables in `.env`, source it and authenticate Azure CLI with: ```console $ az login --service-principal -u $ARM_CLIENT_ID -p $ARM_CLIENT_SECRET --tenant $ARM_TENANT_ID ``` ### Permissions Following permissions are needed for provisioning the infrastructure and running the tests: - `Microsoft.Kubernetes/*` - `Microsoft.Resources/*` - `Microsoft.Authorization/roleAssignments/{Read,Write,Delete}` - `Microsoft.ContainerRegistry/*` - `Microsoft.ContainerService/*` - `Microsoft.KeyVault/*` - `Microsoft.EventHub/*` ### IAM and CI setup To create the necessary IAM role with all the permissions, set up CI secrets and variables using [azure-gh-actions](https://github.com/fluxcd/test-infra/tree/main/tf-modules/azure/github-actions) use the terraform configuration below. Please make sure all the requirements of azure-gh-actions are followed before running it. **NOTE:** When running the following for a repo under an organization, set the environment variable `GITHUB_ORGANIZATION` if setting the `owner` in the `github` provider doesn't work. ```hcl provider "github" { owner = "fluxcd" } resource "tls_private_key" "privatekey" { algorithm = "RSA" rsa_bits = 4096 } module "azure_gh_actions" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/github-actions" azure_owners = ["owner-id-1", "owner-id-2"] azure_app_name = "flux2-e2e" azure_app_description = "flux2 e2e" azure_app_secret_name = "flux2-e2e" azure_permissions = [ "Microsoft.Kubernetes/*", "Microsoft.Resources/*", "Microsoft.Authorization/roleAssignments/Read", "Microsoft.Authorization/roleAssignments/Write", "Microsoft.Authorization/roleAssignments/Delete", "Microsoft.ContainerRegistry/*", "Microsoft.ContainerService/*", "Microsoft.KeyVault/*", "Microsoft.EventHub/*" ] azure_location = "eastus" github_project = "flux2" github_secret_client_id_name = "AZ_ARM_CLIENT_ID" github_secret_client_secret_name = "AZ_ARM_CLIENT_SECRET" github_secret_subscription_id_name = "AZ_ARM_SUBSCRIPTION_ID" github_secret_tenant_id_name = "AZ_ARM_TENANT_ID" github_secret_custom = { "TF_VAR_azuredevops_org" = "", "TF_VAR_azuredevops_pat" = "", "AZURE_GITREPO_SSH_CONTENTS" = base64encode(tls_private_key.privatekey.private_key_openssh), "AZURE_GITREPO_SSH_PUB_CONTENTS" = base64encode(tls_private_key.privatekey.public_key_openssh) } } output "publickey" { value = tls_private_key.privatekey.public_key_openssh } ``` Copy the `publickey` output printed after applying, or run `terraform output` to print it again, and add it in the Azure DevOps SSH public keys under the user account that'll be used by flux in the tests. **NOTE:** The environment variables used above are for the GitHub workflow that runs the tests. Change the variable names if needed accordingly. ## GCP ### Architecture The [gcp](./terraform/gcp) terraform files create the GKE cluster and related resources to run the tests. It creates: - A Google Container Registry and Artifact Registry - A Google Kubernetes Cluster - Two Google Cloud Source Repositories - A Google Pub/Sub Topic and a subscription to the service that would be used in the tests Note: It doesn't create Google KMS keyrings and crypto keys because these cannot be destroyed. Instead, you have to pass in the crypto key and keyring that would be used to test the sops encryption in Flux. Please see `.env.sample` for the terraform variables ### Requirements - GCP account with an active project to be able to create GKE and GCR, and permission to assign roles. - Existing GCP KMS keyring and crypto key. - [Create a Keyring](https://cloud.google.com/kms/docs/create-key-ring) in `global` location. - [Create a Crypto Key](https://cloud.google.com/kms/docs/create-key) with symmetric algorithm for encryption and decryption, and software based protection level. - gcloud CLI, need to be logged in using `gcloud auth login` as a User (not a Service Account), configure application default credentials with `gcloud auth application-default login` and docker credential helper with `gcloud auth configure-docker`. **NOTE:** To use Service Account (for example in CI environment), set `GOOGLE_APPLICATION_CREDENTIALS` variable in `.env` with the path to the JSON key file, source it and authenticate gcloud CLI with: ```console $ gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS ``` Depending on the Container/Artifact Registry host used in the test, authenticate docker accordingly ```console $ gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://us-central1-docker.pkg.dev ``` In this case, the GCP client in terraform uses the Service Account to authenticate and the gcloud CLI is used only to authenticate with Google Container Registry and Google Artifact Registry. **NOTE FOR CI USAGE:** When saving the JSON key file as a CI secret, compress the file content with ```console $ cat key.json | jq -r tostring ``` to prevent aggressive masking in the logs. Refer [aggressive replacement in logs](https://github.com/google-github-actions/auth/blob/v1.1.0/docs/TROUBLESHOOTING.md#aggressive--replacement-in-logs) for more details. - Register [SSH Keys with Google Cloud](https://cloud.google.com/source-repositories/docs/authentication#ssh) - Google Cloud supports these three SSH key types: RSA (only for keys with more than 2048 bits), ECDSA and ED25519. - The SSH user doesn't have to be a member of the GCP project. The terraform setup will grant the user permissions to the repository. Visit https://source.cloud.google.com, login or create a GCP account with the SSH user's email address and add SSH keys in the account. Set this email as the value for the environment variable `TF_VAR_gcp_email` in `.env` file to be used as a terraform variable. **Note:** Google doesn't allow a SSH key to be associated with a service account email address. Therefore, there has to be an actual user that the SSH key is registered to. ### Permissions Following roles are needed for provisioning the infrastructure and running the tests: - Compute Instance Admin (v1) - `roles/compute.instanceAdmin.v1` - Kubernetes Engine Admin - `roles/container.admin` - Service Account User - `roles/iam.serviceAccountUser` - Service Account Token Creator - `roles/iam.serviceAccountTokenCreator` - Artifact Registry Administrator - `roles/artifactregistry.admin` - Artifact Registry Repository Administrator - `roles/artifactregistry.repoAdmin` - Cloud KMS Admin - `roles/cloudkms.admin` - Cloud KMS CryptoKey Encrypter - `roles/cloudkms.cryptoKeyEncrypter` - Source Repository Administrator - `roles/source.admin` - Pub/Sub Admin - `roles/pubsub.admin` ### IAM and CI setup To create the necessary IAM role with all the permissions, set up CI secrets and variables using [gcp-gh-actions](https://github.com/fluxcd/test-infra/tree/main/tf-modules/gcp/github-actions) use the terraform configuration below. Please make sure all the requirements of gcp-gh-actions are followed before running it. **NOTE:** When running the following for a repo under an organization, set the environment variable `GITHUB_ORGANIZATION` if setting the `owner` in the `github` provider doesn't work. ```hcl provider "google" {} provider "github" { owner = "fluxcd" } resource "tls_private_key" "privatekey" { algorithm = "RSA" rsa_bits = 4096 } module "gcp_gh_actions" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/github-actions" gcp_service_account_id = "flux2-e2e-test" gcp_service_account_name = "flux2-e2e-test" gcp_service_account_description = "For running fluxcd/flux2 e2e tests." gcp_roles = [ "roles/compute.instanceAdmin.v1", "roles/container.admin", "roles/iam.serviceAccountUser", "roles/iam.serviceAccountTokenCreator", "roles/artifactregistry.admin", "roles/artifactregistry.repoAdmin", "roles/cloudkms.admin", "roles/cloudkms.cryptoKeyEncrypter", "roles/source.admin", "roles/pubsub.admin" ] github_project = "flux2" github_secret_credentials_name = "FLUX2_E2E_GOOGLE_CREDENTIALS" github_secret_custom = { "TF_VAR_gcp_keyring" = "", "TF_VAR_gcp_crypto_key" = "", "TF_VAR_gcp_email" = "", "GCP_GITREPO_SSH_CONTENTS" = base64encode(tls_private_key.privatekey.private_key_openssh), "GCP_GITREPO_SSH_PUB_CONTENTS" = base64encode(tls_private_key.privatekey.public_key_openssh) } } output "publickey" { value = tls_private_key.privatekey.public_key_openssh } ``` Copy the `publickey` output printed after applying, or run `terraform output` to print it again, and add it in the Google Source Repository SSH public keys under the user account with email address referred in `TF_VAR_gcp_email` above. **NOTE:** The environment variables used above are for the GitHub workflow that runs the tests. Change the variable names if needed accordingly. ## Tests Each test run is initiated by running `terraform apply` in the provider's terraform directory e.g terraform apply, it does this by using the [tftestenv package](https://github.com/fluxcd/test-infra/blob/main/tftestenv/testenv.go) within the `fluxcd/test-infra` repository. It then reads the output of the Terraform to get information needed for the tests like the kubernetes client ID, the cloud repository urls, the key vault ID etc. This means that a lot of the communication with the cloud provider API is offset to Terraform instead of requiring it to be implemented in the test. The following tests are currently implemented: - Flux can be successfully installed on the cluster using the Flux CLI - source-controller can clone cloud provider repositories (Azure DevOps, Google Cloud Source Repositories) (https+ssh) - image-reflector-controller can list tags from provider container Registry image repositories - kustomize-controller can decrypt secrets using SOPS and provider key vault - image-automation-controller can create branches and push to cloud repositories (https+ssh) - source-controller can pull charts from cloud provider container registry Helm repositories - notification-controller can forward events to cloud Events Service(EventHub for Azure and Google Pub/Sub) The following tests are run only for Azure since it is supported in the notification-controller: - notification-controller can send commit status to Azure DevOps ### Running tests locally 1. Ensure that you have the Flux CLI binary that is to be tested built and ready. You can build it by running `make build` at the root of this repository. The binary is located at `./bin` directory at the root and by default this is where the Makefile copies the binary for the tests from. If you have it in a different location, you can set it with the `FLUX_BINARY` variable 2. Copy `.env.sample` to `.env` and add the values for the different variables for the provider that you are running the tests for. 3. Run `make test-`, setting the location of the flux binary with `FLUX_BINARY` variable ```console $ make test-azure make test PROVIDER_ARG="-provider azure" # These two versions of podinfo are pushed to the cloud registry and used in tests for ImageUpdateAutomation mkdir -p build cp ../../bin/flux build/flux docker pull ghcr.io/stefanprodan/podinfo:6.0.0 6.0.0: Pulling from stefanprodan/podinfo Digest: sha256:e7eeab287181791d36c82c904206a845e30557c3a4a66a8143fa1a15655dae97 Status: Image is up to date for ghcr.io/stefanprodan/podinfo:6.0.0 ghcr.io/stefanprodan/podinfo:6.0.0 docker pull ghcr.io/stefanprodan/podinfo:6.0.1 6.0.1: Pulling from stefanprodan/podinfo Digest: sha256:1169f220a670cf640e45e1a7ac42dc381a441e9d4b7396432cadb75beb5b5d68 Status: Image is up to date for ghcr.io/stefanprodan/podinfo:6.0.1 ghcr.io/stefanprodan/podinfo:6.0.1 go test -timeout 60m -v ./ -existing -provider azure --tags=integration 2023/03/24 02:32:25 Setting up azure e2e test infrastructure 2023/03/24 02:32:25 Terraform binary: /usr/local/bin/terraform 2023/03/24 02:32:25 Init Terraform ....[some output has been cut out] 2023/03/24 02:39:33 helm repository condition not ready --- PASS: TestACRHelmRelease (15.31s) === RUN TestKeyVaultSops --- PASS: TestKeyVaultSops (15.98s) PASS 2023/03/24 02:40:12 Destroying environment... ok github.com/fluxcd/flux2/tests/integration 947.341s ``` In the above, the test created a build directory build/ and the flux cli binary is copied build/flux. It would be used to bootstrap Flux on the cluster. You can configure the location of the Flux CLI binary by setting the FLUX_BINARY variable. We also pull two version of `ghcr.io/stefanprodan/podinfo` image. These images are pushed to the cloud provider's Container Registry and used to test `ImageRepository` and `ImageUpdateAutomation`. The terraform resources get created and the tests are run. If not configured explicitly to retain the infrastructure, at the end of the test, the test infrastructure is deleted. In case of any failure due to which the resources don't get deleted, the `make destroy-*` commands can be run for the respective provider. This will run terraform destroy in the respective provider's terraform configuration directory. This can be used to quickly destroy the infrastructure without going through the provision-test-destroy steps. ### Debugging the tests For debugging environment provisioning, enable verbose output with `-verbose` test flag. ```sh make test-azure GO_TEST_ARGS="-verbose" ``` The test environment is destroyed at the end by default. Run the tests with -retain flag to retain the created test infrastructure. ```sh make test-azure GO_TEST_ARGS="-retain" ``` The tests require the infrastructure state to be clean. For re-running the tests with a retained infrastructure, set -existing flag. ```sh make test-azure GO_TEST_ARGS="-retain -existing" ``` To delete an existing infrastructure created with -retain flag: ```sh make test-azure GO_TEST_ARGS="-existing" ``` To debug issues on the cluster created by the test (provided you passed in the `-retain` flag): ```sh export KUBECONFIG=./build/kubeconfig kubectl get pods ``` ================================================ FILE: tests/integration/azure_specific_test.go ================================================ //go:build azure // +build azure /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "io" "log" "strings" "testing" "time" giturls "github.com/chainguard-dev/git-urls" "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/git" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notiv1 "github.com/fluxcd/notification-controller/api/v1" notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestAzureDevOpsCommitStatus(t *testing.T) { g := NewWithT(t) ctx := context.TODO() branchName := "commit-status" testID := branchName + randStringRunes(5) manifest := `apiVersion: v1 kind: ConfigMap metadata: name: foobar` repoUrl := getTransportURL(cfg.applicationRepository) tmpDir := t.TempDir() c, err := getRepository(ctx, tmpDir, repoUrl, defaultBranch, cfg.defaultAuthOpts) g.Expect(err).ToNot(HaveOccurred()) files := make(map[string]io.Reader) files["configmap.yaml"] = strings.NewReader(manifest) err = commitAndPushAll(ctx, c, files, branchName) g.Expect(err).ToNot(HaveOccurred()) modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { spec.HealthChecks = []meta.NamespacedObjectKindReference{ { APIVersion: "v1", Kind: "ConfigMap", Name: "foobar", Namespace: testID, }, } } err = setUpFluxConfig(ctx, testID, nsConfig{ ref: &sourcev1.GitRepositoryRef{ Branch: branchName, }, repoURL: repoUrl, path: "./", modifyKsSpec: modifyKsSpec, }) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(func() { err := tearDownFluxConfig(ctx, testID) if err != nil { log.Printf("failed to delete resources in '%s' namespace: %s", testID, err) } }) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv, testID, testID) if err != nil { return false } return true }, testTimeout, testInterval) secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "azuredevops-token", Namespace: testID, }, StringData: map[string]string{ "token": cfg.gitPat, }, } g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) defer testEnv.Delete(ctx, &secret) provider := notiv1beta3.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: "azuredevops", Namespace: testID, }, Spec: notiv1beta3.ProviderSpec{ Type: "azuredevops", Address: repoUrl, SecretRef: &meta.LocalObjectReference{ Name: "azuredevops-token", }, }, } g.Expect(testEnv.Create(ctx, &provider)).To(Succeed()) defer testEnv.Delete(ctx, &provider) alert := notiv1beta3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: "azuredevops", Namespace: testID, }, Spec: notiv1beta3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: provider.Name, }, EventSources: []notiv1.CrossNamespaceObjectReference{ { Kind: "Kustomization", Name: testID, Namespace: testID, }, }, }, } g.Expect(testEnv.Create(ctx, &alert)).To(Succeed()) defer testEnv.Delete(ctx, &alert) url, err := ParseAzureDevopsURL(repoUrl) g.Expect(err).ToNot(HaveOccurred()) rev, err := c.Head() g.Expect(err).ToNot(HaveOccurred()) connection := azuredevops.NewPatConnection(url.OrgURL, cfg.gitPat) client, err := git.NewClient(ctx, connection) g.Expect(err).ToNot(HaveOccurred()) getArgs := git.GetStatusesArgs{ Project: &url.Project, RepositoryId: &url.Repo, CommitId: &rev, } g.Eventually(func() bool { statuses, err := client.GetStatuses(ctx, getArgs) if err != nil { return false } if len(*statuses) != 1 { return false } return true }, 500*time.Second, 5*time.Second) } type AzureDevOpsURL struct { OrgURL string Project string Repo string } // TODO(somtochiama): move this into fluxcd/pkg and reuse in NC func ParseAzureDevopsURL(s string) (AzureDevOpsURL, error) { var args AzureDevOpsURL u, err := giturls.Parse(s) if err != nil { return args, nil } scheme := u.Scheme if u.Scheme == "ssh" { scheme = "https" } id := strings.TrimLeft(u.Path, "/") id = strings.TrimSuffix(id, ".git") comp := strings.Split(id, "/") if len(comp) != 4 { return args, fmt.Errorf("invalid repository id %q", id) } args = AzureDevOpsURL{ OrgURL: fmt.Sprintf("%s://%s/%s", scheme, u.Host, comp[0]), Project: comp[1], Repo: comp[3], } return args, nil } ================================================ FILE: tests/integration/azure_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "os" eventhub "github.com/Azure/azure-event-hubs-go/v3" "github.com/fluxcd/pkg/git" "github.com/fluxcd/test-infra/tftestenv" tfjson "github.com/hashicorp/terraform-json" ) const ( azureDevOpsKnownHosts = "ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H" ) // createKubeConfigAKS constructs kubeconfig for an AKS cluster from the // terraform state output at the given kubeconfig path. func createKubeConfigAKS(ctx context.Context, state map[string]*tfjson.StateOutput, kcPath string) error { kubeconfigYaml, ok := state["aks_kubeconfig"].Value.(string) if !ok || kubeconfigYaml == "" { return fmt.Errorf("failed to obtain kubeconfig from tf output") } return tftestenv.CreateKubeconfigAKS(ctx, kubeconfigYaml, kcPath) } func getTestConfigAKS(ctx context.Context, outputs map[string]*tfjson.StateOutput) (*testConfig, error) { fleetInfraRepository := outputs["fleet_infra_repository"].Value.(map[string]interface{}) applicationRepository := outputs["application_repository"].Value.(map[string]interface{}) eventHubSas := outputs["event_hub_sas"].Value.(string) sharedSopsId := outputs["sops_id"].Value.(string) kustomizeYaml := ` resources: - gotk-components.yaml - gotk-sync.yaml patchesStrategicMerge: - |- apiVersion: apps/v1 kind: Deployment metadata: name: kustomize-controller namespace: flux-system spec: template: spec: containers: - name: manager env: - name: AZURE_AUTH_METHOD value: msi ` privateKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPath) if !ok { return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPath) } privateKeyData, err := os.ReadFile(privateKeyFile) if err != nil { return nil, fmt.Errorf("error getting azure devops private key, '%s': %w", privateKeyFile, err) } pubKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPubPath) if !ok { return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPubPath) } pubKeyData, err := os.ReadFile(pubKeyFile) if err != nil { return nil, fmt.Errorf("error getting ssh pubkey '%s', %w", pubKeyFile, err) } c := make(chan []byte, 10) closefn, err := setupEventHubHandler(ctx, c, eventHubSas) var notificationCfg = notificationConfig{ notificationChan: c, providerType: "azureeventhub", closeChan: closefn, secret: map[string]string{ "address": eventHubSas, }, } config := &testConfig{ defaultGitTransport: git.HTTP, gitUsername: git.DefaultPublicKeyAuthUser, gitPat: outputs["azure_devops_access_token"].Value.(string), gitPrivateKey: string(privateKeyData), gitPublicKey: string(pubKeyData), knownHosts: azureDevOpsKnownHosts, fleetInfraRepository: gitUrl{ http: fleetInfraRepository["http"].(string), ssh: fleetInfraRepository["ssh"].(string), }, applicationRepository: gitUrl{ http: applicationRepository["http"].(string), ssh: applicationRepository["ssh"].(string), }, notificationCfg: notificationCfg, sopsArgs: fmt.Sprintf("--azure-kv %s", sharedSopsId), sopsSecretData: map[string]string{ "sops.azure-kv": fmt.Sprintf(`clientId: %s`, outputs["aks_client_id"].Value.(string)), }, kustomizationYaml: kustomizeYaml, } opts, err := authOpts(config.fleetInfraRepository.http, map[string][]byte{ "password": []byte(config.gitPat), "username": []byte("git"), }) if err != nil { return nil, err } config.defaultAuthOpts = opts return config, nil } // registryLoginACR logs into the Azure Container Registries using the // provider's CLI tools and returns the test repositories. func registryLoginACR(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) { // NOTE: ACR registry accept dynamic repository creation by just pushing a // new image with a new repository name. registryURL := output["acr_url"].Value.(string) if err := tftestenv.RegistryLoginACR(ctx, registryURL); err != nil { return "", err } return registryURL, nil } func setupEventHubHandler(ctx context.Context, c chan []byte, eventHubSas string) (func(), error) { hub, err := eventhub.NewHubFromConnectionString(eventHubSas) if err != nil { return nil, err } handler := func(ctx context.Context, event *eventhub.Event) error { c <- event.Data return nil } runtimeInfo, err := hub.GetRuntimeInformation(ctx) if err != nil { return nil, err } listenerHandler, err := hub.Receive(ctx, runtimeInfo.PartitionIDs[0], handler, eventhub.ReceiveWithLatestOffset()) if err != nil { return nil, err } closefn := func() { listenerHandler.Close(ctx) hub.Close(ctx) } return closefn, nil } ================================================ FILE: tests/integration/flux_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "io" "strings" "testing" "time" "github.com/fluxcd/pkg/git" sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) func TestFluxInstallation(t *testing.T) { g := NewWithT(t) ctx := context.TODO() t.Cleanup(func() { dumpDiagnostics(t, ctx, "flux-system") }) g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv.Client, "flux-system", "flux-system") if err != nil { return false } return true }, 60*time.Second, 5*time.Second) } func TestRepositoryCloning(t *testing.T) { ctx := context.TODO() branchName := "feature/branch" tagName := "v1" g := NewWithT(t) type testStruct struct { name string refType string cloneType git.TransportType } tests := []testStruct{ { name: "ssh-feature-branch", refType: "branch", cloneType: git.SSH, }, { name: "ssh-v1", refType: "tag", cloneType: git.SSH, }, } // Not all cloud providers have repositories that support authentication with an accessToken // we don't run http tests for these. if cfg.gitPat != "" { httpTests := []testStruct{ { name: "https-feature-branch", refType: "branch", cloneType: git.HTTP, }, { name: "https-v1", refType: "tag", cloneType: git.HTTP, }, } tests = append(tests, httpTests...) } t.Log("Creating application sources") url := getTransportURL(cfg.applicationRepository) tmpDir := t.TempDir() client, err := getRepository(ctx, tmpDir, url, defaultBranch, cfg.defaultAuthOpts) g.Expect(err).ToNot(HaveOccurred()) files := make(map[string]io.Reader) for _, tt := range tests { manifest := `apiVersion: v1 kind: ConfigMap metadata: name: foobar ` name := fmt.Sprintf("cloning-test/%s/configmap.yaml", tt.name) files[name] = strings.NewReader(manifest) } err = commitAndPushAll(ctx, client, files, branchName) g.Expect(err).ToNot(HaveOccurred()) err = createTagAndPush(ctx, client, branchName, tagName) g.Expect(err).ToNot(HaveOccurred()) t.Log("Verifying application-gitops namespaces") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) ctx := context.TODO() ref := &sourcev1.GitRepositoryRef{ Branch: branchName, } if tt.refType == "tag" { ref = &sourcev1.GitRepositoryRef{ Tag: tagName, } } url := cfg.applicationRepository.http if tt.cloneType == git.SSH { url = cfg.applicationRepository.ssh } testID := fmt.Sprintf("%s-%s", tt.name, randStringRunes(5)) err := setUpFluxConfig(ctx, testID, nsConfig{ repoURL: url, ref: ref, protocol: tt.cloneType, objectName: testID, path: fmt.Sprintf("./cloning-test/%s", tt.name), }) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(func() { err := tearDownFluxConfig(ctx, testID) if err != nil { t.Logf("failed to delete resources in '%s' namespace: %s", tt.name, err) } }) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) if err != nil { return false } return true }, 120*time.Second, 5*time.Second).Should(BeTrue()) // Wait for configmap to be deployed g.Eventually(func() bool { nn := types.NamespacedName{Name: "foobar", Namespace: testID} cm := &corev1.ConfigMap{} err = testEnv.Get(ctx, nn, cm) if err != nil { return false } return true }, 120*time.Second, 5*time.Second).Should(BeTrue()) }) } } ================================================ FILE: tests/integration/gcp_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "io" "log" "os" "strings" "cloud.google.com/go/pubsub" tfjson "github.com/hashicorp/terraform-json" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/fluxcd/pkg/git" "github.com/fluxcd/test-infra/tftestenv" ) const ( gcpSourceRepoKnownHosts = "[source.developers.google.com]:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=" ) // createKubeConfigGKE constructs kubeconfig for a GKE cluster from the // terraform state output at the given kubeconfig path. func createKubeConfigGKE(ctx context.Context, state map[string]*tfjson.StateOutput, kcPath string) error { kubeconfigYaml, ok := state["gke_kubeconfig"].Value.(string) if !ok || kubeconfigYaml == "" { return fmt.Errorf("failed to obtain kubeconfig from tf output") } return tftestenv.CreateKubeconfigGKE(ctx, kubeconfigYaml, kcPath) } // registryLoginGCR logs into the Artifact registries using the gcloud // and returns a list of test repositories. func registryLoginGCR(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) { project := output["gcp_project_id"].Value.(string) region := output["gcp_region"].Value.(string) repositoryID := output["artifact_registry_id"].Value.(string) artifactRegistryURL, artifactRepoURL := tftestenv.GetGoogleArtifactRegistryAndRepository(project, region, repositoryID) if err := tftestenv.RegistryLoginGCR(ctx, artifactRegistryURL); err != nil { return "", err } return artifactRepoURL, nil } func getTestConfigGKE(ctx context.Context, outputs map[string]*tfjson.StateOutput) (*testConfig, error) { sharedSopsId := outputs["sops_id"].Value.(string) privateKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPath) if !ok { return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPath) } privateKeyData, err := os.ReadFile(privateKeyFile) if err != nil { return nil, fmt.Errorf("error getting gcp source repositories private key, '%s': %w", privateKeyFile, err) } pubKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPubPath) if !ok { return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPubPath) } pubKeyData, err := os.ReadFile(pubKeyFile) if err != nil { return nil, fmt.Errorf("error getting ssh pubkey '%s', %w", pubKeyFile, err) } c := make(chan []byte, 10) projectID := outputs["gcp_project_id"].Value.(string) topicID := outputs["pubsub_topic"].Value.(string) fn, err := setupPubSubReceiver(ctx, c, projectID, topicID) if err != nil { return nil, err } var notificationCfg = notificationConfig{ providerType: "googlepubsub", providerChannel: topicID, notificationChan: c, closeChan: fn, secret: map[string]string{ "address": projectID, }, } config := &testConfig{ defaultGitTransport: git.SSH, gitUsername: "git", gitPrivateKey: string(privateKeyData), gitPublicKey: string(pubKeyData), knownHosts: gcpSourceRepoKnownHosts, fleetInfraRepository: gitUrl{ ssh: outputs["fleet_infra_repository"].Value.(string), }, applicationRepository: gitUrl{ ssh: outputs["application_repository"].Value.(string), }, notificationCfg: notificationCfg, sopsArgs: fmt.Sprintf("--gcp-kms %s", sharedSopsId), } opts, err := authOpts(config.fleetInfraRepository.ssh, map[string][]byte{ "identity": []byte(config.gitPrivateKey), "known_hosts": []byte(config.knownHosts), }) if err != nil { return nil, err } config.defaultAuthOpts = opts // In Azure, the repository is initialized with a default branch through // terraform. We have to do it manually here for GCP to prevent errors // when trying to clone later. We only need to do it for the application repository // since flux bootstrap pushes to the main branch. files := make(map[string]io.Reader) files["README.md"] = strings.NewReader("# Flux test repo") tmpDir, err := os.MkdirTemp("", "*-flux-test") if err != nil { return nil, err } defer os.RemoveAll(tmpDir) client, err := getRepository(context.Background(), tmpDir, config.applicationRepository.ssh, defaultBranch, config.defaultAuthOpts) if err != nil { return nil, err } err = commitAndPushAll(context.Background(), client, files, defaultBranch) if err != nil { return nil, err } return config, nil } func setupPubSubReceiver(ctx context.Context, c chan []byte, projectID string, topicID string) (func(), error) { newCtx, cancel := context.WithCancel(ctx) pubsubClient, err := pubsub.NewClient(newCtx, projectID) if err != nil { cancel() return nil, fmt.Errorf("error creating pubsub client: %s", err) } sub := pubsubClient.Subscription(topicID) go func() { err = sub.Receive(ctx, func(ctx context.Context, message *pubsub.Message) { c <- message.Data message.Ack() }) if err != nil && status.Code(err) != codes.Canceled { log.Printf("error receiving message in subscription: %s\n", err) return } }() return func() { cancel() pubsubClient.Close() }, nil } ================================================ FILE: tests/integration/go.mod ================================================ module github.com/fluxcd/flux2/tests/integration go 1.26.0 require ( cloud.google.com/go/pubsub v1.50.1 github.com/Azure/azure-event-hubs-go/v3 v3.6.2 github.com/chainguard-dev/git-urls v1.0.2 github.com/fluxcd/helm-controller/api v1.4.5 github.com/fluxcd/image-automation-controller/api v1.0.4 github.com/fluxcd/image-reflector-controller/api v1.0.4 github.com/fluxcd/kustomize-controller/api v1.7.3 github.com/fluxcd/notification-controller/api v1.7.5 github.com/fluxcd/pkg/apis/event v0.25.0 github.com/fluxcd/pkg/apis/meta v1.26.0 github.com/fluxcd/pkg/git v0.46.0 github.com/fluxcd/pkg/runtime v0.103.0 github.com/fluxcd/source-controller/api v1.7.4 github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b github.com/go-git/go-git/v5 v5.16.5 github.com/google/go-containerregistry v0.20.7 github.com/hashicorp/terraform-exec v0.24.0 github.com/hashicorp/terraform-json v0.27.2 github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 github.com/onsi/gomega v1.39.0 google.golang.org/grpc v1.77.0 k8s.io/api v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/client-go v0.35.2 sigs.k8s.io/controller-runtime v0.23.3 ) require ( cloud.google.com/go v0.121.6 // indirect cloud.google.com/go/auth v0.16.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/pubsub/v2 v2.0.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-amqp-common-go/v4 v4.2.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/go-amqp v1.4.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.30 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.2 // indirect github.com/Azure/go-autorest/logger v0.2.2 // indirect github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/devigned/tab v0.1.1 // indirect github.com/docker/cli v29.0.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect github.com/fluxcd/pkg/ssh v0.24.0 // indirect github.com/fluxcd/pkg/version v0.14.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.16.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.247.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: tests/integration/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v4 v4.2.0 h1:q/jLx1KJ8xeI8XGfkOWMN9XrXzAfVTkyvCxPvHCjd2I= github.com/Azure/azure-amqp-common-go/v4 v4.2.0/go.mod h1:GD3m/WPPma+621UaU6KNjKEo5Hl09z86viKwQjTpV0Q= github.com/Azure/azure-event-hubs-go/v3 v3.6.2 h1:7rNj1/iqS/i3mUKokA2n2eMYO72TB7lO7OmpbKoakKY= github.com/Azure/azure-event-hubs-go/v3 v3.6.2/go.mod h1:n+ocYr9j2JCLYqUqz9eI+lx/TEAtL/g6rZzyTFSuIpc= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk= github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U= github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= github.com/Azure/go-autorest/autorest/validation v0.3.2 h1:myD3tcvs+Fk1bkJ1Xx7xidop4z4FWvWADiMGMXeVd2E= github.com/Azure/go-autorest/autorest/validation v0.3.2/go.mod h1:4z7eU88lSINAB5XL8mhfPumiUdoAQo/c7qXwbsM8Zhc= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/elazarl/goproxy v1.8.0 h1:dt561rX7UAYMeFRLtzFx6uQGl2TpL1dr6uCG23nFQSY= github.com/elazarl/goproxy v1.8.0/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= github.com/fluxcd/helm-controller/api v1.4.5 h1:hMEBtgXUbJjp+ah0jPI3OOQNVngoToOQvTgFgVpAjNg= github.com/fluxcd/helm-controller/api v1.4.5/go.mod h1:rCgx3qhjjtoIH+1EbzFC2vN71/pp0PgMDrZnGCZX5XY= github.com/fluxcd/image-automation-controller/api v1.0.4 h1:Fgdy97hXkyh/JFjxLIyq4ZDHsKsa49aumtrvIyjVd08= github.com/fluxcd/image-automation-controller/api v1.0.4/go.mod h1:LLBf4XQJAgnpIMlZUwfpVIkCdUtBOi31B6fDbPwBCq4= github.com/fluxcd/image-reflector-controller/api v1.0.4 h1:/JGpTZf4eMcKG2FpWfP5H7SneSrD5P8EvwGnHiH/WLY= github.com/fluxcd/image-reflector-controller/api v1.0.4/go.mod h1:5GS4ojHaz+W6hK80WakGIOYk8sn93AyV5X+YOne1XMw= github.com/fluxcd/kustomize-controller/api v1.7.3 h1:g+C9Il+H33DQi/ZiQ8KpTvL9KXebXnS4oM/0uJ/C8Gw= github.com/fluxcd/kustomize-controller/api v1.7.3/go.mod h1:Yj80JyfQpBUgLhsUZ/c86qcvPGO2+P1VCKsb8fL+L/k= github.com/fluxcd/notification-controller/api v1.7.5 h1:6CO5bKyjodiK9exQFOdBcz0XLeo17rrrWQBTJL9NNa8= github.com/fluxcd/notification-controller/api v1.7.5/go.mod h1:IciwSg8Q0pVtdbsyDyEXx/MxBKWeagxAazpm64C8oCE= github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA= github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4= github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE= github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y= github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE= github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw= github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI= github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8= github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw= github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4= github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg= github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw= github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs= github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY= github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b h1:FSPtvaVgL8azcyweqLmD71elAw4vozuXH/QvsJQ7tg0= github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b/go.mod h1:liFlLEXgambGVdWSJ4JzbIHf1Vjpp1HwUyPazPIVZug= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps= go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0= k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: tests/integration/image_repo_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "bytes" "context" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" automationv1 "github.com/fluxcd/image-automation-controller/api/v1" reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestImageRepositoryAndAutomation(t *testing.T) { g := NewWithT(t) ctx := context.TODO() branchName := "image-repository" testID := branchName + "-" + randStringRunes(5) imageURL := fmt.Sprintf("%s/podinfo", cfg.testRegistry) manifest := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: podinfo namespace: %[1]s spec: selector: matchLabels: app: podinfo template: metadata: labels: app: podinfo spec: containers: - name: podinfod image: %[2]s:%[3]s # {"$imagepolicy": "%[1]s:podinfo"} readinessProbe: exec: command: - podcli - check - http - localhost:9898/readyz initialDelaySeconds: 5 timeoutSeconds: 5 `, testID, imageURL, oldPodinfoVersion) repoUrl := getTransportURL(cfg.applicationRepository) client, err := getRepository(ctx, t.TempDir(), repoUrl, defaultBranch, cfg.defaultAuthOpts) g.Expect(err).ToNot(HaveOccurred()) files := make(map[string]io.Reader) files[testID+"/podinfo.yaml"] = strings.NewReader(manifest) err = commitAndPushAll(ctx, client, files, branchName) g.Expect(err).ToNot(HaveOccurred()) err = setUpFluxConfig(ctx, testID, nsConfig{ repoURL: repoUrl, path: testID, ref: &sourcev1.GitRepositoryRef{ Branch: branchName, }, }) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(func() { err := tearDownFluxConfig(ctx, testID) if err != nil { t.Logf("failed to delete resources in '%s' namespace: %s", testID, err) } }) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) if err != nil { return false } return true }, testTimeout, testInterval).Should(BeTrue()) imageRepository := reflectorv1.ImageRepository{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: testID, }, Spec: reflectorv1.ImageRepositorySpec{ Image: imageURL, Interval: metav1.Duration{ Duration: 1 * time.Minute, }, Provider: infraOpts.Provider, }, } g.Expect(testEnv.Create(ctx, &imageRepository)).To(Succeed()) defer testEnv.Delete(ctx, &imageRepository) imagePolicy := reflectorv1.ImagePolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: testID, }, Spec: reflectorv1.ImagePolicySpec{ ImageRepositoryRef: meta.NamespacedObjectReference{ Name: imageRepository.Name, }, Policy: reflectorv1.ImagePolicyChoice{ SemVer: &reflectorv1.SemVerPolicy{ Range: "6.0.x", }, }, }, } g.Expect(testEnv.Create(ctx, &imagePolicy)).To(Succeed()) defer testEnv.Delete(ctx, &imagePolicy) imageAutomation := automationv1.ImageUpdateAutomation{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: testID, }, Spec: automationv1.ImageUpdateAutomationSpec{ Interval: metav1.Duration{ Duration: 1 * time.Minute, }, SourceRef: automationv1.CrossNamespaceSourceReference{ Kind: "GitRepository", Name: testID, }, GitSpec: &automationv1.GitSpec{ Checkout: &automationv1.GitCheckoutSpec{ Reference: sourcev1.GitRepositoryRef{ Branch: branchName, }, }, Commit: automationv1.CommitSpec{ Author: automationv1.CommitUser{ Email: "imageautomation@example.com", Name: "imageautomation", }, }, }, Update: &automationv1.UpdateStrategy{ Path: testID, Strategy: automationv1.UpdateStrategySetters, }, }, } g.Expect(testEnv.Create(ctx, &imageAutomation)).To(Succeed()) defer testEnv.Delete(ctx, &imageAutomation) // Wait for image repository to be ready g.Eventually(func() bool { client, err := getRepository(ctx, t.TempDir(), repoUrl, branchName, cfg.defaultAuthOpts) if err != nil { return false } b, err := os.ReadFile(filepath.Join(client.Path(), testID, "podinfo.yaml")) if err != nil { return false } if bytes.Contains(b, []byte(newPodinfoVersion)) == false { return false } return true }, testTimeout, testInterval).Should(BeTrue()) } ================================================ FILE: tests/integration/notification_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "encoding/json" "io" "strings" "testing" "time" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notiv1 "github.com/fluxcd/notification-controller/api/v1" notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" events "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestNotification(t *testing.T) { g := NewWithT(t) ctx := context.TODO() branchName := "test-notification" testID := branchName + "-" + randStringRunes(5) defer cfg.notificationCfg.closeChan() // Setup Flux resources manifest := `apiVersion: v1 kind: ConfigMap metadata: name: foobar` repoUrl := getTransportURL(cfg.applicationRepository) client, err := getRepository(ctx, t.TempDir(), repoUrl, defaultBranch, cfg.defaultAuthOpts) g.Expect(err).ToNot(HaveOccurred()) files := make(map[string]io.Reader) files["configmap.yaml"] = strings.NewReader(manifest) err = commitAndPushAll(ctx, client, files, branchName) g.Expect(err).ToNot(HaveOccurred()) namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: testID, }, } g.Expect(testEnv.Create(ctx, &namespace)).To(Succeed()) defer testEnv.Delete(ctx, &namespace) provider := notiv1beta3.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: testID, Namespace: testID, }, Spec: notiv1beta3.ProviderSpec{ Type: cfg.notificationCfg.providerType, Address: cfg.notificationCfg.providerAddress, Channel: cfg.notificationCfg.providerChannel, }, } if cfg.notificationCfg.secret != nil { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: testID, Namespace: testID, }, StringData: cfg.notificationCfg.secret, } g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) defer testEnv.Delete(ctx, &secret) provider.Spec.SecretRef = &meta.LocalObjectReference{ Name: testID, } } g.Expect(testEnv.Create(ctx, &provider)).To(Succeed()) defer testEnv.Delete(ctx, &provider) alert := notiv1beta3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: testID, Namespace: testID, }, Spec: notiv1beta3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: provider.Name, }, EventSources: []notiv1.CrossNamespaceObjectReference{ { Kind: "Kustomization", Name: testID, Namespace: testID, }, }, }, } g.Expect(testEnv.Create(ctx, &alert)).ToNot(HaveOccurred()) defer testEnv.Delete(ctx, &alert) modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { spec.Interval = metav1.Duration{Duration: 30 * time.Second} spec.HealthChecks = []meta.NamespacedObjectKindReference{ { APIVersion: "v1", Kind: "ConfigMap", Name: "foobar", Namespace: testID, }, } } g.Expect(setUpFluxConfig(ctx, testID, nsConfig{ repoURL: repoUrl, ref: &sourcev1.GitRepositoryRef{ Branch: branchName, }, path: "./", modifyKsSpec: modifyKsSpec, })).To(Succeed()) t.Cleanup(func() { err := tearDownFluxConfig(ctx, testID) if err != nil { t.Logf("failed to delete resources in '%s' namespace: %s", testID, err) } }) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv, testID, testID) if err != nil { t.Log(err) return false } return true }, testTimeout, testInterval).Should(BeTrue()) // Wait to read event from notification channel. g.Eventually(func() bool { select { case eventJson := <-cfg.notificationCfg.notificationChan: event := &events.Event{} err := json.Unmarshal([]byte(eventJson), event) if err != nil { t.Logf("the received event type does not match Flux format, error: %v", err) return false } if event.InvolvedObject.Kind == kustomizev1.KustomizationKind && event.InvolvedObject.Name == testID && event.InvolvedObject.Namespace == testID { return true } return false default: return false } }, testTimeout, 1*time.Second).Should(BeTrue()) } ================================================ FILE: tests/integration/oci_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "strings" "testing" "time" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" helmv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestOCIHelmRelease(t *testing.T) { g := NewWithT(t) ctx := context.TODO() // Create namespace for test testID := "oci-helm-" + randStringRunes(5) namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: testID, }, } g.Expect(testEnv.Create(ctx, &namespace)).To(Succeed()) defer testEnv.Delete(ctx, &namespace) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) repoURL := fmt.Sprintf("%s/charts/podinfo", cfg.testRegistry) err := pushImagesFromURL(repoURL, "ghcr.io/stefanprodan/charts/podinfo:6.2.0", []string{"6.2.0"}) g.Expect(err).ToNot(HaveOccurred()) // Create HelmRepository. helmRepository := sourcev1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{Name: testID, Namespace: testID}, Spec: sourcev1.HelmRepositorySpec{ URL: fmt.Sprintf("oci://%s", cfg.testRegistry), Interval: metav1.Duration{ Duration: 5 * time.Minute, }, Provider: infraOpts.Provider, PassCredentials: true, Type: "oci", }, } g.Expect(testEnv.Create(ctx, &helmRepository)).To(Succeed()) defer testEnv.Delete(ctx, &helmRepository) // create helm release helmRelease := helmv2.HelmRelease{ ObjectMeta: metav1.ObjectMeta{Name: testID, Namespace: testID}, Spec: helmv2.HelmReleaseSpec{ Chart: &helmv2.HelmChartTemplate{ Spec: helmv2.HelmChartTemplateSpec{ Interval: &metav1.Duration{ Duration: 10 * time.Minute, }, Chart: "charts/podinfo", Version: "6.2.0", SourceRef: helmv2.CrossNamespaceObjectReference{ Kind: sourcev1.HelmRepositoryKind, Name: helmRepository.Name, Namespace: helmRepository.Namespace, }, }, }, }, } g.Expect(testEnv.Create(ctx, &helmRelease)).To(Succeed()) defer testEnv.Delete(ctx, &helmRelease) g.Eventually(func() bool { chart := &sourcev1.HelmChart{} nn := types.NamespacedName{ Name: fmt.Sprintf("%s-%s", helmRelease.Name, helmRelease.Namespace), Namespace: helmRelease.Namespace, } if err := testEnv.Get(ctx, nn, chart); err != nil { t.Logf("error getting helm chart: %s", err.Error()) return false } if err := checkReadyCondition(chart); err != nil { t.Logf("HelmChart not ready: %s", err) return false } obj := &helmv2.HelmRelease{} nn = types.NamespacedName{Name: helmRelease.Name, Namespace: helmRelease.Namespace} if err := testEnv.Get(ctx, nn, obj); err != nil { t.Logf("error getting helm release: %s", err.Error()) return false } if err := checkReadyCondition(obj); err != nil { // Log all HelmRelease conditions for full picture. var condSummary []string for _, c := range obj.Status.Conditions { condSummary = append(condSummary, fmt.Sprintf("%s=%s (%s)", c.Type, c.Status, c.Message)) } t.Logf("HelmRelease not ready: conditions=[%s]", strings.Join(condSummary, "; ")) // Log pod states in the release namespace. logNamespacePods(t, ctx, testID) return false } return true }, testTimeout, testInterval).Should(BeTrue()) } ================================================ FILE: tests/integration/sops_encryption_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "io" "log" "os" "testing" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/test-infra/tftestenv" ) func TestKeyVaultSops(t *testing.T) { g := NewWithT(t) ctx := context.TODO() branchName := "key-vault" testID := branchName + "-" + randStringRunes(5) secretYaml := `apiVersion: v1 kind: Secret metadata: name: "test" stringData: foo: "bar"` repoUrl := getTransportURL(cfg.applicationRepository) tmpDir := t.TempDir() client, err := getRepository(ctx, tmpDir, repoUrl, defaultBranch, cfg.defaultAuthOpts) g.Expect(err).ToNot(HaveOccurred()) dir := client.Path() + "/key-vault-sops" g.Expect(os.Mkdir(dir, 0o700)).To(Succeed()) filename := dir + "secret.enc.yaml" f, err := os.Create(filename) g.Expect(err).ToNot(HaveOccurred()) defer f.Close() _, err = f.Write([]byte(secretYaml)) g.Expect(err).ToNot(HaveOccurred()) g.Expect(f.Sync()).To(Succeed()) err = tftestenv.RunCommand(ctx, client.Path(), fmt.Sprintf("sops --encrypt --encrypted-regex '^(data|stringData)$' %s --in-place %s", cfg.sopsArgs, filename), tftestenv.RunCommandOptions{}) g.Expect(err).ToNot(HaveOccurred()) r, err := os.Open(filename) g.Expect(err).ToNot(HaveOccurred()) files := make(map[string]io.Reader) files["key-vault-sops/secret.enc.yaml"] = r err = commitAndPushAll(ctx, client, files, branchName) g.Expect(err).ToNot(HaveOccurred()) modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { spec.Decryption = &kustomizev1.Decryption{ Provider: "sops", } if cfg.sopsSecretData != nil { spec.Decryption.SecretRef = &meta.LocalObjectReference{ Name: "sops-keys", } } } err = setUpFluxConfig(ctx, testID, nsConfig{ ref: &sourcev1.GitRepositoryRef{ Branch: branchName, }, repoURL: repoUrl, path: "./key-vault-sops", modifyKsSpec: modifyKsSpec, protocol: cfg.defaultGitTransport, }) g.Expect(err).ToNot(HaveOccurred()) t.Cleanup(func() { err := tearDownFluxConfig(ctx, testID) if err != nil { log.Printf("failed to delete resources in '%s' namespace", testID) } }) t.Cleanup(func() { dumpDiagnostics(t, ctx, testID) }) if cfg.sopsSecretData != nil { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "sops-keys", Namespace: testID, }, StringData: cfg.sopsSecretData, } g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) defer testEnv.Delete(ctx, &secret) } g.Eventually(func() bool { err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) if err != nil { return false } nn := types.NamespacedName{Name: "test", Namespace: testID} secret := &corev1.Secret{} err = testEnv.Get(ctx, nn, secret) if err != nil { return false } if string(secret.Data["foo"]) == "bar" { return true } return false }, testTimeout, testInterval).Should(BeTrue()) } ================================================ FILE: tests/integration/suite_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "flag" "fmt" "log" "math/rand" "os" "testing" "time" "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" helmv2 "github.com/fluxcd/helm-controller/api/v2" automationv1 "github.com/fluxcd/image-automation-controller/api/v1" reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/git" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/test-infra/tftestenv" ) const ( // azureTerraformPath is the path to the folder containing the // terraform files for azure infra azureTerraformPath = "./terraform/azure" // gcpTerraformPath is the path to the folder containing the // terraform files for gcp infra gcpTerraformPath = "./terraform/gcp" // kubeconfigPath is the path of the file containing the kubeconfig kubeconfigPath = "./build/kubeconfig" // fluxBin is the path to the flux binary. fluxBin = "./build/flux" // default branch to be used when cloning git repositories defaultBranch = "main" // envVarGitRepoSSHPath is the environment variable that contains the path // to the ssh key for the git repository envVarGitRepoSSHPath = "GITREPO_SSH_PATH" // envVarGitRepoSSHPubPath is the environment variable that contains the path // to the ssh public key for the git repository envVarGitRepoSSHPubPath = "GITREPO_SSH_PUB_PATH" ) var ( // supportedProviders are the providers supported by the test. supportedProviders = []string{"azure", "gcp"} // cfg is a struct containing different variables needed for the test. cfg *testConfig // infraOpts are the options for running the terraform environment infraOpts tftestenv.Options // versions to tag and push for the podinfo image oldPodinfoVersion = "6.0.0" newPodinfoVersion = "6.0.1" podinfoTags = []string{oldPodinfoVersion, newPodinfoVersion} // testEnv is the test environment. It contains test infrastructure and // kubernetes client of the created cluster. testEnv *tftestenv.Environment // testTimeout is used as a timeout when testing a condition with gomega's eventually testTimeout = 60 * time.Second // testInterval is used as an interval when testing a condition with gomega's eventually testInterval = 5 * time.Second random *rand.Rand letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") localImg = "ghcr.io/stefanprodan/podinfo" ) // testConfig hold different variable that will be needed by the different test functions. type testConfig struct { // authentication info for git repositories gitPat string gitUsername string gitPrivateKey string gitPublicKey string defaultGitTransport git.TransportType defaultAuthOpts *git.AuthOptions knownHosts string fleetInfraRepository gitUrl applicationRepository gitUrl // sopsArgs is the cloud provider dependent argument to pass to the sops cli sopsArgs string // notificationCfg contains the values needed to properly set up notification on the // cluster. notificationCfg notificationConfig // sopsSecretData is the secret's data for the sops decryption sopsSecretData map[string]string // kustomizationYaml is the content of the kustomization.yaml for customizing the Flux manifests kustomizationYaml string // testRegistry is the registry of the cloud provider. testRegistry string } // notificationConfig contains various fields for configuring // providers and testing notifications for the different // cloud providers. type notificationConfig struct { providerChannel string providerType string providerAddress string secret map[string]string notificationChan chan []byte closeChan func() } // gitUrl contains the http/ssh urls for the created git repositories // on the various cloud providers. type gitUrl struct { http string ssh string } // getTestConfig gets the test configuration that contains different variables for running the tests type getTestConfig func(ctx context.Context, output map[string]*tfjson.StateOutput) (*testConfig, error) // registryLoginFunc is used to perform registry login against a provider based // on the terraform state output values. It returns the test registry // to test against, read from the terraform state output. type registryLoginFunc func(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) // providerConfig contains the test configurations for the different cloud providers type providerConfig struct { terraformPath string createKubeconfig tftestenv.CreateKubeconfig getTestConfig getTestConfig // registryLogin is used to perform registry login. registryLogin registryLoginFunc } func init() { utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme)) utilruntime.Must(helmv2.AddToScheme(scheme.Scheme)) utilruntime.Must(reflectorv1.AddToScheme(scheme.Scheme)) utilruntime.Must(automationv1.AddToScheme(scheme.Scheme)) utilruntime.Must(notiv1beta3.AddToScheme(scheme.Scheme)) random = rand.New(rand.NewSource(time.Now().UnixNano())) } func TestMain(m *testing.M) { ctx := context.TODO() infraOpts.Bindflags(flag.CommandLine) flag.Parse() // Validate the provider. if infraOpts.Provider == "" { log.Fatalf("-provider flag must be set to one of %v", supportedProviders) } var supported bool for _, p := range supportedProviders { if p == infraOpts.Provider { supported = true break } } if !supported { log.Fatalf("Unsupported provider %q, must be one of %v", infraOpts.Provider, supportedProviders) } // get provider specific configuration providerCfg := getProviderConfig(infraOpts.Provider) if providerCfg == nil { log.Fatalf("Failed to get provider config for %q", infraOpts.Provider) } // Run destroy-only mode if enabled. if infraOpts.DestroyOnly { log.Println("Running in destroy-only mode...") envOpts := []tftestenv.EnvironmentOption{ tftestenv.WithVerbose(infraOpts.Verbose), // Ignore any state lock in destroy-only mode. tftestenv.WithTfDestroyOptions(tfexec.Lock(false)), } if err := tftestenv.Destroy(ctx, providerCfg.terraformPath, envOpts...); err != nil { panic(err) } os.Exit(0) } // Initialize with non-zero exit code to indicate failure by default unless // set by a successful test run. exitCode := 1 // Setup Terraform binary and init state log.Printf("Setting up %s e2e test infrastructure", infraOpts.Provider) envOpts := []tftestenv.EnvironmentOption{ tftestenv.WithExisting(infraOpts.Existing), tftestenv.WithRetain(infraOpts.Retain), tftestenv.WithVerbose(infraOpts.Verbose), tftestenv.WithCreateKubeconfig(providerCfg.createKubeconfig), } // Create terraform infrastructure var err error testEnv, err = tftestenv.New(ctx, scheme.Scheme, providerCfg.terraformPath, kubeconfigPath, envOpts...) if err != nil { log.Fatalf("Failed to provision the test infrastructure: %v", err) } defer func() { if err := testEnv.Stop(ctx); err != nil { log.Printf("Failed to stop environment: %v", err) exitCode = 1 } // Log the panic error before exit to surface the cause of panic. if err := recover(); err != nil { log.Printf("panic: %v", err) } os.Exit(exitCode) }() // get terrraform infrastructure outputs, err := testEnv.StateOutput(ctx) if err != nil { panic(fmt.Sprintf("Failed to get the terraform state output: %v", err)) } // get provider specific test configuration cfg, err = providerCfg.getTestConfig(ctx, outputs) if err != nil { panic(fmt.Sprintf("Failed to get test config: %v", err)) } regUrl, err := providerCfg.registryLogin(ctx, outputs) if err != nil { panic(fmt.Sprintf("Failed to log into registry: %v", err)) } cfg.testRegistry = regUrl err = pushTestImages(ctx, cfg.testRegistry, podinfoTags) if err != nil { panic(fmt.Sprintf("Failed to push test images: %v", err)) } tmpDir, err := os.MkdirTemp("", "*-flux-test") if err != nil { panic(fmt.Sprintf("Failed to create tmp dir: %v", err)) } defer func() { err := os.RemoveAll(tmpDir) if err != nil { log.Printf("error removing tmp dir: %s\n", err) } }() log.Println("Installing flux") err = installFlux(ctx, tmpDir, kubeconfigPath) defer func() { log.Println("Uninstalling Flux") if err := uninstallFlux(ctx); err != nil { log.Printf("Failed to uninstall: %v", err) } }() if err != nil { panic(fmt.Sprintf("error installing Flux: %v", err)) } // On check failure, log and continue. Controllers may be ready by the time // tests run. log.Println("Running flux check") if err := runFluxCheck(ctx); err != nil { log.Printf("flux check failed: %v\n", err) } log.Println("Running e2e tests") exitCode = m.Run() } func getProviderConfig(provider string) *providerConfig { switch provider { case "azure": return &providerConfig{ terraformPath: azureTerraformPath, createKubeconfig: createKubeConfigAKS, getTestConfig: getTestConfigAKS, registryLogin: registryLoginACR, } case "gcp": return &providerConfig{ terraformPath: gcpTerraformPath, createKubeconfig: createKubeConfigGKE, getTestConfig: getTestConfigGKE, registryLogin: registryLoginGCR, } } return nil } // pushTestImages pushes the local podinfo image to the remote repository specified // by repoURL. The image should be existing on the machine. func pushTestImages(ctx context.Context, repoURL string, tags []string) error { for _, tag := range tags { remoteImg := fmt.Sprintf("%s/podinfo:%s", repoURL, tag) err := tftestenv.RetagAndPush(ctx, fmt.Sprintf("%s:%s", localImg, tag), remoteImg) if err != nil { return err } } return nil } func randStringRunes(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[random.Intn(len(letterRunes))] } return string(b) } ================================================ FILE: tests/integration/terraform/azure/aks.tf ================================================ module "aks" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/aks" name = local.name location = var.azure_location tags = var.tags } module "acr" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/acr" name = local.name location = var.azure_location aks_principal_id = [module.aks.principal_id] resource_group = module.aks.resource_group admin_enabled = true tags = var.tags depends_on = [module.aks] } ================================================ FILE: tests/integration/terraform/azure/azuredevops.tf ================================================ resource "azuredevops_project" "e2e" { name = local.name visibility = "private" version_control = "Git" work_item_template = "Agile" description = "Test Project for Flux E2E test - Managed by Terraform" } resource "azuredevops_git_repository" "fleet_infra" { project_id = azuredevops_project.e2e.id name = "fleet-infra-${local.name}" default_branch = "refs/heads/main" initialization { init_type = "Clean" } } resource "azuredevops_git_repository" "application" { project_id = azuredevops_project.e2e.id name = "application-${local.name}" default_branch = "refs/heads/main" initialization { init_type = "Clean" } } ================================================ FILE: tests/integration/terraform/azure/event-hub.tf ================================================ resource "azurerm_eventhub_namespace" "this" { name = local.name location = var.azure_location resource_group_name = module.aks.resource_group sku = "Basic" capacity = 1 tags = var.tags } resource "azurerm_eventhub" "this" { name = local.name namespace_name = azurerm_eventhub_namespace.this.name resource_group_name = module.aks.resource_group partition_count = 1 message_retention = 1 } resource "azurerm_eventhub_authorization_rule" "this" { name = local.name resource_group_name = module.aks.resource_group namespace_name = azurerm_eventhub_namespace.this.name eventhub_name = azurerm_eventhub.this.name listen = true send = true manage = false } ================================================ FILE: tests/integration/terraform/azure/keyvault.tf ================================================ resource "azurerm_key_vault" "this" { name = local.name resource_group_name = module.aks.resource_group location = var.azure_location tenant_id = data.azurerm_client_config.current.tenant_id sku_name = "standard" tags = var.tags } resource "azurerm_key_vault_access_policy" "admin" { key_vault_id = azurerm_key_vault.this.id tenant_id = data.azurerm_client_config.current.tenant_id object_id = data.azurerm_client_config.current.object_id key_permissions = [ "Create", "Update", "Encrypt", "Delete", "Get", "List", "Purge", "Recover", "GetRotationPolicy", "SetRotationPolicy" ] secret_permissions = [ "Get", "Delete", "Purge", "Recover" ] } resource "azurerm_key_vault_access_policy" "cluster_binding" { key_vault_id = azurerm_key_vault.this.id tenant_id = data.azurerm_client_config.current.tenant_id object_id = module.aks.principal_id key_permissions = [ "Decrypt", "Encrypt", ] } resource "azurerm_key_vault_key" "sops" { depends_on = [azurerm_key_vault_access_policy.admin] name = "sops" key_vault_id = azurerm_key_vault.this.id key_type = "RSA" key_size = 2048 tags = var.tags key_opts = [ "decrypt", "encrypt", ] } ================================================ FILE: tests/integration/terraform/azure/main.tf ================================================ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = ">=3.20.0" } azuread = { source = "hashicorp/azuread" version = ">=2.28.0" } azuredevops = { source = "microsoft/azuredevops" version = ">=0.2.2" } } } provider "azurerm" { features {} } provider "azuredevops" { org_service_url = "https://dev.azure.com/${var.azuredevops_org}" personal_access_token = var.azuredevops_pat } data "azurerm_client_config" "current" {} resource "random_pet" "suffix" { separator = "o" } locals { name = "e2e${random_pet.suffix.id}" } ================================================ FILE: tests/integration/terraform/azure/outputs.tf ================================================ output "aks_kubeconfig" { description = "kubeconfig of the created AKS cluster" value = module.aks.kubeconfig sensitive = true } output "azure_devops_access_token" { sensitive = true value = var.azuredevops_pat } output "fleet_infra_repository" { value = { http = azuredevops_git_repository.fleet_infra.remote_url ssh = "ssh://git@ssh.dev.azure.com/v3/${var.azuredevops_org}/${azuredevops_git_repository.fleet_infra.project_id}/${azuredevops_git_repository.fleet_infra.name}" } } output "application_repository" { value = { http = azuredevops_git_repository.application.remote_url ssh = "ssh://git@ssh.dev.azure.com/v3/${var.azuredevops_org}/${azuredevops_git_repository.application.project_id}/${azuredevops_git_repository.application.name}" } } output "aks_client_id" { value = module.aks.kubelet_client_id } output "event_hub_sas" { value = azurerm_eventhub_authorization_rule.this.primary_connection_string sensitive = true } output "sops_id" { value = azurerm_key_vault_key.sops.id } output "acr_url" { value = module.acr.registry_url } ================================================ FILE: tests/integration/terraform/azure/variables.tf ================================================ variable "azuredevops_org" { type = string description = "Name of Azure DevOps organizations were the repositories will be created" } variable "azure_location" { type = string description = "Location of the resource group" default = "eastus" } variable "tags" { type = map(string) default = {} description = "Tags for created Azure resources" } variable "azuredevops_pat" { type = string description = "Personal access token for Azure DevOps repository" } ================================================ FILE: tests/integration/terraform/gcp/gke.tf ================================================ module "gke" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/gke" name = local.name tags = var.tags } module "gcr" { source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/gcr" name = local.name tags = var.tags } ================================================ FILE: tests/integration/terraform/gcp/kms.tf ================================================ data "google_kms_key_ring" "keyring" { name = var.gcp_keyring location = "global" } data "google_kms_crypto_key" "my_crypto_key" { name = var.gcp_crypto_key key_ring = data.google_kms_key_ring.keyring.id } resource "google_kms_key_ring_iam_binding" "key_ring" { key_ring_id = data.google_kms_key_ring.keyring.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" members = [ "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com", ] } ================================================ FILE: tests/integration/terraform/gcp/main.tf ================================================ provider "google" { project = var.gcp_project_id region = var.gcp_region zone = var.gcp_zone } resource "random_pet" "suffix" {} locals { name = "e2e-${random_pet.suffix.id}" } data "google_project" "project" {} ================================================ FILE: tests/integration/terraform/gcp/outputs.tf ================================================ output "gke_kubeconfig" { value = module.gke.kubeconfig sensitive = true } output "gcp_project_id" { value = var.gcp_project_id } output "gcp_region" { value = var.gcp_region } output "artifact_registry_id" { value = module.gcr.artifact_repository_id } output "sops_id" { value = data.google_kms_crypto_key.my_crypto_key.id } output "fleet_infra_repository" { value = "ssh://${var.gcp_email}@source.developers.google.com:2022/p/${var.gcp_project_id}/r/${google_sourcerepo_repository.fleet-infra.name}" } output "application_repository" { value = "ssh://${var.gcp_email}@source.developers.google.com:2022/p/${var.gcp_project_id}/r/${google_sourcerepo_repository.application.name}" } output "pubsub_topic" { value = google_pubsub_topic.pubsub.name } ================================================ FILE: tests/integration/terraform/gcp/pubsub.tf ================================================ resource "google_pubsub_topic" "pubsub" { name = local.name labels = var.tags message_retention_duration = "7200s" } resource "google_pubsub_subscription" "sub" { project = var.gcp_project_id name = local.name topic = google_pubsub_topic.pubsub.name } ================================================ FILE: tests/integration/terraform/gcp/sourcerepo.tf ================================================ resource "google_sourcerepo_repository" "fleet-infra" { name = "fleet-infra-${random_pet.suffix.id}" } resource "google_sourcerepo_repository" "application" { name = "application-${random_pet.suffix.id}" } resource "google_sourcerepo_repository_iam_binding" "application_binding" { project = google_sourcerepo_repository.application.project repository = google_sourcerepo_repository.application.name role = "roles/source.admin" members = [ "user:${var.gcp_email}", ] } resource "google_sourcerepo_repository_iam_binding" "fleet-infra_binding" { project = google_sourcerepo_repository.fleet-infra.project repository = google_sourcerepo_repository.fleet-infra.name role = "roles/source.admin" members = [ "user:${var.gcp_email}", ] } ================================================ FILE: tests/integration/terraform/gcp/variables.tf ================================================ variable "gcp_project_id" { type = string description = "GCP project to create the resources in" } variable "gcp_email" { type = string description = "GCP user email" } variable "gcp_region" { type = string default = "us-central1" description = "GCP region" } variable "gcp_zone" { type = string default = "us-central1" description = "GCP zone" } variable "gcp_keyring" { type = string description = "GCP keyring that contains crypto key for encrypting secrets" } variable "gcp_crypto_key" { type = string description = "GCP crypto key for encrypting secrets" } variable "tags" { type = map(string) default = {} description = "tags for created resources" } ================================================ FILE: tests/integration/util_test.go ================================================ /* Copyright 2023 The Flux authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "errors" "fmt" "io" "net/url" "os" "strings" "testing" "time" extgogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/google/go-containerregistry/pkg/crane" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/controller-runtime/pkg/client" helmv2 "github.com/fluxcd/helm-controller/api/v2" automationv1 "github.com/fluxcd/image-automation-controller/api/v1" reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" "github.com/fluxcd/pkg/git/repository" "github.com/fluxcd/pkg/runtime/conditions" sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/fluxcd/test-infra/tftestenv" ) // installFlux adds the core Flux components to the cluster specified in the kubeconfig file. func installFlux(ctx context.Context, tmpDir string, kubeconfigPath string) error { // Create flux-system namespace namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "flux-system", }, } err := testEnv.Create(ctx, &namespace) if err != nil { return err } repoURL := getTransportURL(cfg.fleetInfraRepository) if cfg.kustomizationYaml != "" { files := make(map[string]io.Reader) files["clusters/e2e/flux-system/kustomization.yaml"] = strings.NewReader(cfg.kustomizationYaml) files["clusters/e2e/flux-system/gotk-components.yaml"] = strings.NewReader("") files["clusters/e2e/flux-system/gotk-sync.yaml"] = strings.NewReader("") c, err := getRepository(ctx, tmpDir, repoURL, defaultBranch, cfg.defaultAuthOpts) if err != nil { return err } err = commitAndPushAll(ctx, c, files, defaultBranch) if err != nil { return err } } var bootstrapArgs string if cfg.defaultGitTransport == git.SSH { f, err := os.CreateTemp("", "flux-e2e-ssh-key-*") if err != nil { return err } err = os.WriteFile(f.Name(), []byte(cfg.gitPrivateKey), 0o600) if err != nil { return err } bootstrapArgs = fmt.Sprintf("--private-key-file=%s -s", f.Name()) } else { bootstrapArgs = fmt.Sprintf("--token-auth --password=%s", cfg.gitPat) } bootstrapCmd := fmt.Sprintf("%s bootstrap git --url=%s %s --kubeconfig=%s --path=clusters/e2e "+ " --components-extra image-reflector-controller,image-automation-controller", fluxBin, repoURL, bootstrapArgs, kubeconfigPath) return tftestenv.RunCommand(ctx, "./", bootstrapCmd, tftestenv.RunCommandOptions{ Timeout: 15 * time.Minute, }) } func runFluxCheck(ctx context.Context) error { checkCmd := fmt.Sprintf("%s check --kubeconfig %s", fluxBin, kubeconfigPath) return tftestenv.RunCommand(ctx, "./", checkCmd, tftestenv.RunCommandOptions{ AttachConsole: true, }) } func uninstallFlux(ctx context.Context) error { uninstallCmd := fmt.Sprintf("%s uninstall --kubeconfig %s -s", fluxBin, kubeconfigPath) if err := tftestenv.RunCommand(ctx, "./", uninstallCmd, tftestenv.RunCommandOptions{ Timeout: 15 * time.Minute, }); err != nil { return err } return nil } // verifyGitAndKustomization checks that the gitrespository and kustomization combination are working properly. func verifyGitAndKustomization(ctx context.Context, kubeClient client.Client, namespace, name string) error { nn := types.NamespacedName{ Name: name, Namespace: namespace, } source := &sourcev1.GitRepository{} if err := kubeClient.Get(ctx, nn, source); err != nil { return err } if err := checkReadyCondition(source); err != nil { return err } kustomization := &kustomizev1.Kustomization{} if err := kubeClient.Get(ctx, nn, kustomization); err != nil { return err } if err := checkReadyCondition(kustomization); err != nil { return err } return nil } type nsConfig struct { repoURL string ref *sourcev1.GitRepositoryRef protocol git.TransportType objectName string path string modifyKsSpec func(spec *kustomizev1.KustomizationSpec) } // setUpFluxConfigs creates the namespace, then creates the git secret, // git repository and kustomization in that namespace func setUpFluxConfig(ctx context.Context, name string, opts nsConfig) error { transport := cfg.defaultGitTransport if opts.protocol != "" { transport = opts.protocol } namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, } if err := testEnv.Create(ctx, &namespace); err != nil && !apierrors.IsAlreadyExists(err) { return err } secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "git-credentials", Namespace: name, }, } secret.StringData = map[string]string{ "username": cfg.gitUsername, "password": cfg.gitPat, } if transport == git.SSH { secret.StringData = map[string]string{ "identity": cfg.gitPrivateKey, "identity.pub": cfg.gitPublicKey, "known_hosts": cfg.knownHosts, } } if err := testEnv.Create(ctx, &secret); err != nil { return err } ref := &sourcev1.GitRepositoryRef{ Branch: name, } if opts.ref != nil { ref = opts.ref } gitSpec := &sourcev1.GitRepositorySpec{ Interval: metav1.Duration{ Duration: 1 * time.Minute, }, Reference: ref, SecretRef: &meta.LocalObjectReference{ Name: secret.Name, }, URL: opts.repoURL, } source := &sourcev1.GitRepository{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}, Spec: *gitSpec, } if err := testEnv.Create(ctx, source); err != nil { return err } ksSpec := &kustomizev1.KustomizationSpec{ Path: opts.path, TargetNamespace: name, SourceRef: kustomizev1.CrossNamespaceSourceReference{ Kind: sourcev1.GitRepositoryKind, Name: source.Name, Namespace: source.Namespace, }, Interval: metav1.Duration{ Duration: 1 * time.Minute, }, Prune: true, } if opts.modifyKsSpec != nil { opts.modifyKsSpec(ksSpec) } kustomization := &kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}, Spec: *ksSpec, } return testEnv.Create(ctx, kustomization) } func tearDownFluxConfig(ctx context.Context, name string) error { var allErr []error source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} if err := testEnv.Delete(ctx, source); err != nil { allErr = append(allErr, err) } kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} if err := testEnv.Delete(ctx, kustomization); err != nil { allErr = append(allErr, err) } namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, } if err := testEnv.Delete(ctx, &namespace); err != nil { allErr = append(allErr, err) } return kerrors.NewAggregate(allErr) } // getRepository and clones the git repository to the directory. func getRepository(ctx context.Context, dir, repoURL, branchName string, authOpts *git.AuthOptions) (*gogit.Client, error) { c, err := gogit.NewClient(dir, authOpts, gogit.WithSingleBranch(false), gogit.WithDiskStorage()) if err != nil { return nil, err } _, err = c.Clone(ctx, repoURL, repository.CloneConfig{ CheckoutStrategy: repository.CheckoutStrategy{ Branch: branchName, }, }) if err != nil { return nil, err } return c, nil } // commitAndPushAll checks out to the specified branch, creates the files, commits and then pushes them to // the remote git repository. func commitAndPushAll(ctx context.Context, client *gogit.Client, files map[string]io.Reader, branchName string) error { err := client.SwitchBranch(ctx, branchName) if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) { return err } _, err = client.Commit(git.Commit{ Author: git.Signature{ Name: git.DefaultPublicKeyAuthUser, Email: "test@example.com", When: time.Now(), }, }, repository.WithFiles(files)) if err != nil { if errors.Is(err, git.ErrNoStagedFiles) { return nil } return err } err = client.Push(ctx, repository.PushConfig{}) if err != nil { return fmt.Errorf("unable to push: %s", err) } return nil } func createTagAndPush(ctx context.Context, client *gogit.Client, branchName, newTag string) error { repo, err := extgogit.PlainOpen(client.Path()) if err != nil { return err } ref, err := repo.Reference(plumbing.NewBranchReferenceName(branchName), false) if err != nil { return err } tags, err := repo.TagObjects() if err != nil { return err } err = tags.ForEach(func(tag *object.Tag) error { if tag.Name == newTag { err = repo.DeleteTag(tag.Name) if err != nil { return err } } return nil }) if err != nil { return fmt.Errorf("error deleting local tag: %w", err) } // Delete remote tag if err := client.Push(ctx, repository.PushConfig{ Refspecs: []string{fmt.Sprintf(":refs/tags/%s", newTag)}, Force: true, }); err != nil && !errors.Is(err, extgogit.NoErrAlreadyUpToDate) { return fmt.Errorf("unable to delete existing tag: %w", err) } sig := &object.Signature{ Name: git.DefaultPublicKeyAuthUser, Email: "test@example.com", When: time.Now(), } if _, err = repo.CreateTag(newTag, ref.Hash(), &extgogit.CreateTagOptions{ Tagger: sig, Message: "create tag", }); err != nil { return fmt.Errorf("unable to create tag: %w", err) } return client.Push(ctx, repository.PushConfig{ Refspecs: []string{"refs/tags/*:refs/tags/*"}, }) } func pushImagesFromURL(repoURL, imgURL string, tags []string) error { img, err := crane.Pull(imgURL) if err != nil { return err } for _, tag := range tags { if err := crane.Push(img, fmt.Sprintf("%s:%s", repoURL, tag)); err != nil { return err } } return nil } func getTransportURL(urls gitUrl) string { if cfg.defaultGitTransport == git.SSH { return urls.ssh } return urls.http } func authOpts(repoURL string, authData map[string][]byte) (*git.AuthOptions, error) { u, err := url.Parse(repoURL) if err != nil { return nil, err } return git.NewAuthOptions(*u, authData) } // checkReadyCondition checks for a Ready condition, it returns nil if the condition is true // or an error (with the message if the Ready condition is present). func checkReadyCondition(from conditions.Getter) error { if conditions.IsReady(from) { return nil } errMsg := "object not ready" readyMsg := conditions.GetMessage(from, meta.ReadyCondition) if readyMsg != "" { errMsg += ": " + readyMsg } return errors.New(errMsg) } // dumpDiagnostics prints Flux object states and controller logs when a test // has failed. It should be registered via t.Cleanup so that it runs after the // test body completes. func dumpDiagnostics(t *testing.T, ctx context.Context, namespace string) { t.Helper() if !t.Failed() { return } t.Log("=== Diagnostics dump (test failed) ===") dumpFluxObjects(t, ctx, namespace) dumpControllerLogs(t, ctx) t.Log("=== End diagnostics dump ===") } // dumpFluxObjects lists Flux custom resources in the given namespace and prints // their status conditions. func dumpFluxObjects(t *testing.T, ctx context.Context, namespace string) { t.Helper() listOpts := &client.ListOptions{Namespace: namespace} gitRepos := &sourcev1.GitRepositoryList{} if err := testEnv.List(ctx, gitRepos, listOpts); err == nil { for _, r := range gitRepos.Items { logObjectStatus(t, "GitRepository", r.Name, r.Namespace, r.Status.Conditions) } } helmRepos := &sourcev1.HelmRepositoryList{} if err := testEnv.List(ctx, helmRepos, listOpts); err == nil { for _, r := range helmRepos.Items { logObjectStatus(t, "HelmRepository", r.Name, r.Namespace, r.Status.Conditions) } } helmCharts := &sourcev1.HelmChartList{} if err := testEnv.List(ctx, helmCharts, listOpts); err == nil { for _, r := range helmCharts.Items { logObjectStatus(t, "HelmChart", r.Name, r.Namespace, r.Status.Conditions) } } kustomizations := &kustomizev1.KustomizationList{} if err := testEnv.List(ctx, kustomizations, listOpts); err == nil { for _, r := range kustomizations.Items { logObjectStatus(t, "Kustomization", r.Name, r.Namespace, r.Status.Conditions) } } helmReleases := &helmv2.HelmReleaseList{} if err := testEnv.List(ctx, helmReleases, listOpts); err == nil { for _, r := range helmReleases.Items { logObjectStatus(t, "HelmRelease", r.Name, r.Namespace, r.Status.Conditions) } } imageRepos := &reflectorv1.ImageRepositoryList{} if err := testEnv.List(ctx, imageRepos, listOpts); err == nil { for _, r := range imageRepos.Items { logObjectStatus(t, "ImageRepository", r.Name, r.Namespace, r.Status.Conditions) } } imagePolicies := &reflectorv1.ImagePolicyList{} if err := testEnv.List(ctx, imagePolicies, listOpts); err == nil { for _, r := range imagePolicies.Items { logObjectStatus(t, "ImagePolicy", r.Name, r.Namespace, r.Status.Conditions) } } imageAutomations := &automationv1.ImageUpdateAutomationList{} if err := testEnv.List(ctx, imageAutomations, listOpts); err == nil { for _, r := range imageAutomations.Items { logObjectStatus(t, "ImageUpdateAutomation", r.Name, r.Namespace, r.Status.Conditions) } } } // logObjectStatus prints the status conditions of a Flux object. func logObjectStatus(t *testing.T, kind, name, namespace string, conditions []metav1.Condition) { t.Helper() t.Logf(" %s/%s (ns: %s):", kind, name, namespace) for _, c := range conditions { t.Logf(" %s: %s — %s (since %s)", c.Type, c.Status, c.Message, c.LastTransitionTime.Format(time.RFC3339)) } } // dumpControllerLogs prints the logs of all Flux controller pods in the // flux-system namespace. func dumpControllerLogs(t *testing.T, ctx context.Context) { t.Helper() podList, err := testEnv.ClientGo.CoreV1().Pods("flux-system").List(ctx, metav1.ListOptions{}) if err != nil { t.Logf("failed to list flux-system pods: %v", err) return } for _, pod := range podList.Items { logs, err := testEnv.ClientGo. CoreV1(). Pods(pod.Namespace). GetLogs(pod.Name, &corev1.PodLogOptions{}). DoRaw(ctx) if err != nil { t.Logf("failed to get logs for pod %s: %v", pod.Name, err) continue } t.Logf("=== Logs for pod %s ===\n%s", pod.Name, string(logs)) } } // logNamespacePods logs the state of all pods in the given namespace, // including container statuses and recent events. Useful for understanding // why a Helm install is stuck. func logNamespacePods(t *testing.T, ctx context.Context, namespace string) { t.Helper() podList := &corev1.PodList{} if err := testEnv.List(ctx, podList, &client.ListOptions{Namespace: namespace}); err != nil { t.Logf(" failed to list pods in %s: %v", namespace, err) return } if len(podList.Items) == 0 { t.Logf(" no pods in namespace %s", namespace) return } for _, pod := range podList.Items { t.Logf(" pod %s: phase=%s", pod.Name, pod.Status.Phase) for _, cs := range pod.Status.ContainerStatuses { if cs.State.Waiting != nil { t.Logf(" container %s: waiting — %s: %s", cs.Name, cs.State.Waiting.Reason, cs.State.Waiting.Message) } else if cs.State.Terminated != nil { t.Logf(" container %s: terminated — %s (exit %d)", cs.Name, cs.State.Terminated.Reason, cs.State.Terminated.ExitCode) } else if cs.State.Running != nil { t.Logf(" container %s: running (ready=%v)", cs.Name, cs.Ready) } } } // Log recent events in the namespace for scheduling/pull failures. events, err := testEnv.ClientGo.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) if err != nil { t.Logf(" failed to list events in %s: %v", namespace, err) return } if len(events.Items) > 0 { t.Logf(" events in namespace %s:", namespace) for _, e := range events.Items { t.Logf(" %s %s/%s: %s — %s", e.Type, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message) } } }